Intro
For our demonstration we want to allow a user to select a single fruit from a list of available fruits. Later on we will be improving it with autocomplete behavior. In this post we’ll start with a naive select
-based filter and show how to convert that to a fully encapsulated Dropdown component.
We will start with a parent component, Filters.vue
which will hold our various filter implementations and indicate what the currently selected fruit is:
Naive filter
The simplest approach for letting a user select a fruit is to add a select
to our template and set the v-model
attribute to selectedFruit
.
This does the trick and may be all that is required for a given use-case.
However, if we begin to enhance the behavior of this drop-down we will find that we are filling up the parent component Filters
with drop-down-specific code. Additionally, if we want the same behavior with a different drop-down and a different set of options we will run into duplication of code.
Our goal is to re-factor this drop-down in a DRY way so that over time we can improve the behavior of the drop-down without having to constantly rewrite the behavior of any parent component which includes it.
We’ll start by capturing the existing implementation as a stand-alone component.
Dropdown component
Basic setup
Let’s start with an empty Dropdown.vue
file and add just the select
element and options to the file:
Custom options
Since we want our component to be generic, we need to factor out the options into a prop. The available options will be provided to the Dropdown component by its parent component.
The structure of the options prop will be an object where keys are unique human-readable strings which will be used as the text of the option. Values can be almost anything. Vue.js supports binding arbitrary objects as selected values in a drop-down.
The Project filter I mentioned in Part 1 used fully populated Project objects from the Teamwork API as values with the project name as the string-key.
In our case the values will be strings which exactly match the fruit names.
In Dropdown.vue
we add our options
prop:
In the template we use v-for
to loop over and render the options:
Note the use of the object key syntax for v-for
.
We can now add the component to the Filters.vue
template:
and specify the options:
Implement v-model
We can now see our select
but it doesn’t support reactively updating the selected value when the drop-down value is changed. It is straightforward to add support for v-model
binding in a custom Vue.js component.
All your component needs to do is:
- Provide a “value” prop which is used by the parent to set the value of your component
- Emit an “input” event when the value of your component changes
However, the specifics of this can trip you up, particularly when you are starting simple and expanding to a more complex implementation.
There are some general principles which will help to avoid pitfalls:
- Determine how your component’s current value will be represented (e.g. string, number, custom object).
emit
an “input” event when the selected value changes (how and when you do this may change depending on the details of your component).- Provide a
value
prop which accepts valid values for your component. - Ensure that the controls in your component reflect this value when mounted and when the value of the prop changes.
Making sure you follow the above principles will give you correct behavior for simple cases like our drop-down as well as the more complex cases we will see later.
For our drop-down, the current value can be pretty much anything since the options are provided by the parent. We’ll keep that in mind as we continue.
The only time our component’s value changes is when the select
element changes. We can handle #2 with a single event listener bound to the select
’s @input
event:
Adding the prop for #3 is simple:
In our implementation the parent component is responsible for setting value
and for providing the set of valid options. We set the prop to null which means any valid type is supported.
If you are implementing v-model
support in a component that is responsible for its own options you would likely be more restrictive with this property definition and potentially include a validation callback here.
Finally, we need to make sure that the select
element updates to reflect the value
prop when it changes. The simplest way to do this is to create a data property to represent the currently selected value:
and use v-model
to bind it to the select
:
With that in place we can handle #4 by setting selectedOption
in a mounted()
method and in a watcher for the value
prop:
You might be wondering at this point why we didn’t just
v-model
theselect
element to thevalue
. One reason, is that doing this will causevalue
to be modified whenever theselect
is changed. Vue.js will throw a warning when this occurs. Thevalue
prop should never be modified directly. You signal value changes to the parent by emitting theinput
event.The other reason not to
v-model
the value prop is that your component’s internal representation will likely be different from the representation which is used in thevalue
prop. You need to have the ability to transform those representations and usingv-model
in this way couples those values too closely together. In fact, when we add autocomplete later on we will remove theselect
and thev-model
attribute entirely.
With the above in place we can now add the v-model
directive to our component in Filters.vue
:
You should be able to change the value of selectedFruit
by changing either drop-down and both drop-downs should update to reflect the currently selected value.
We are now in a great position to enhance this component with autocomplete functionality.
Comments
This was really helpful.
This was really helpful. Thanks!
Small suggestion, you can
Small suggestion, you can eliminate the need for your mounted lifecycle hook with the watch immediate setting by doing something like the following (formatting is being lost):
watch: {
value: {
immediate: true,
handler: function (newValue) {
this.selectedOption = newValue
}
}
}
seem to bee running off the
seem to bee running off the screen in Internet explorer.I'm nnot sure iff thiss is a format issue or something to do with web broser compatibility but I thought I'd post tto let you know. The style and design ook great though! Hope you get the issue solved soon.
Add new comment