Getting your component interface right is the most important thing.
And you can't leave it for later.
You have to get it right from the very start.
But why is this so important?
An interface is like a contract that your component makes with whoever is using it.
If you give me these inputs, these are what the outputs will be. Or in Vue terms, if you give me these props, this is what will be rendered.
As your beautifully written component is used more and more, you have more places that rely on this component. They're expecting that it will do what it says it will.
It may only be 2 other components that rely on it. But it may be hundreds if the component is part of a component library.
So how do things go wrong?
If you break the contract that's defined by your component's interface, you'll break anything that relies on your component.
This is obviously bad!
You need to keep that interface consistent to avoid those headaches.
And if you keep the interface the same -- the props and slots, and what the component renders -- you can change how it all works internally and keep your promises. You can refactor and rewrite the internals of the component, and no one else cares.
Getting the interface right is a lot of pressure when you're writing your brand new component!
But you don't have to stress out over this.
The amount you need to worry about this is based on how much the component will be used.
If it's going to be used in only 2 spots, it's not as painful if you make mistakes. But if Vuetify gets an interface wrong, that's a completely different story.
Most people don't think about the interface enough.
And that's why I'm stressing this so much.
So spend more time getting the interface right!
Let's see how we can do that.
It's a pretty simple trick, really.
Instead of just jumping in and writing the component, you take a step back to think.
Simple, but not always easy!
You need to think about what it is that you want to build. Then you dream up the perfect way it should be used. You have to imagine you're in a magical world where anything can come true.
In a perfect world...
...wouldn't it be amazing if Git never had any merge conflicts?
...or if your code worked flawlessly on the first try?
In my perfect world Internet Explorer would no longer exist! (We're getting there slowly but surely).
But we're talking about Vue components here, so let's get on with it.
As we're dreaming up our ideal interface, there are a few things we'll want to keep in mind:
So let's just jump in and see what this might look like with a few different components!
We'll start off with something most of you are familiar with.
If we wanted to build our own Radio
and RadioGroup
components, what would we need?
Our basic requirements would look something like this:
Radio
can be toggled on and offRadio
has a labelRadio
components togetherRadio
in a group can be toggled on at a timeLet's start with what a Radio
component could look like:
<Radio label="Option 1" />
Seems straightforward so far, so let's look at what a bunch of Radio
components in a RadioGroup
might look like for selecting t-shirt sizes. We'll restrict the options to Small
, Medium
, or Large
:
<template><div>Select a shirt size:<RadioGroup><Radio label="Small" /><Radio label="Medium" /><Radio label="Large" /></RadioGroup></div></template>
Not too bad!
But right now we have no way of knowing what radio button was clicked, or setting a default selected value. The easiest way to do that would be by adding a v-model
to it:
<template><div>Select a shirt size:<RadioGroup v-model="shirtSize"><Radio label="Small" /><Radio label="Medium" /><Radio label="Large" /></RadioGroup></div></template>
export default {name: 'SelectShirt',components: {RadioGroup,Radio,},data() {return {// Default to mediumshirtSize: 'Medium',}}}
Then when the user clicks on a Radio
component, it will update the value of shirtSize
in our SelectShirt
component.
Seems like a pretty solid interface!
When we go to actually write this component, we may find things don't work out the way we had thought. But that's all part of the process.
You should expect to iterate on the design and slowly refine it.
Like sculpting a statue out of a slab of marble.
A Media Object is a pattern where you have an image, a header, and some text grouped together.
The most obvious way to create an interface for this component is this:
<MediaObjectimage="path/to/image"title="Media Heading"description="This is the description of the item"/>
But let's look at the prop names and see if we can do better.
To specify the image we have image
. But would it make more sense to have it be src
instead? This would make it more consistent with the <img>
tag we already have in HTML.
We could even change title
to be heading
. I'm not sure which would be more intuitive though 🤔.
The prop description
could even be changed to text
if you wanted.
As you can see, even something that seems as simple as prop names can be difficult to figure out!
You could also make this component far more composable using slots. But we'll cover that in the next example.
Our Autocomplete
will need to be given a list of items, and then it will tell us which one is selected. Perhaps we would use it something like this:
<Autocomplete:items="['Apple', 'Orange', 'Banana']"@selected="handleSelect"/>
Then the select
event returns the item that was selected:
handleSelect(item) {console.log(item) // 'Apple'}
This will work pretty well, but only in cases where we know all possible values ahead of time. Things like selecting a car model, country, or favourite genre of music:
<template><div>What kind of music do you like?<Autocomplete:items="genres"@selected="handleSelect"/></div></template>
export default {name: 'MusicTaste',components: { Autocomplete },data() {return {genres: ['Rock', 'EDM', 'Jazz', 'Country', 'R&B'],};},methods: {handleSelect(genre) {console.log(`You like to listen to ${genre} music!`);},}}
But maybe we will want our Autocomplete
to be hooked up to an API so we can fetch that data dynamically. For this we'll have to do these steps:
Autocomplete
We don't have to worry about how exactly this will work (just yet), but we know we'll need another event for when the user types:
<Autocomplete:items="['Apple', 'Orange', 'Banana']"@input="handleInput"@selected="handleSelect"/>
Awesome! This let's us do a dynamic search that updates as we type, like Google's autocomplete does:
And this is just one way of doing it. We could have the Autocomplete
handle all of the logic of fetching results from the API if we wanted to.
Let's take this to the next level now:
What if we want to customize how each item is rendered in the Autocomplete
?
We'll have to use slots for that, specifically scoped slots. Probably something that would look like this:
<template><Autocomplete:items="['Apple', 'Orange', 'Banana']"@input="handleInput"@selected="handleSelect"><div v-slot="{ item }" class="custom-list-item">{{ item }}</div></Autocomplete></template>
<style>.custom-list-item {color: navy:font-weight: bold;}</style>
Like with the RadioGroup
example, as we start to implement this Autocomplete
we'll likely find places where we need to tweak our design.
Hopefully seeing this in action has helped you to understand how you can start with the perfect interface.
By stopping to think first, we can save ourselves -- and others -- a lot of time and headache.
If you learned something in this article, please share it with someone who would also love to learn!