Let's continue where we left off from the first part and finish up our component.
In this second, and final, part, we'll add the necessary keyboard and mouse interactions, implement selecting an option, and filter the displayed options depending on what's typed in in the input.
Right now, we can see that the options dropdown is always open. This, obviously, isn't the way it should be. The dropdown should be shown only when the user types something in the input. And it should be closed when the user clears the input, presses escape key, or blurs the input.
The first step to accomplish this is to add isOpen
in the component's data list and set its default value to false
.
data () {
return {
isOpen: false
}
}
With that being added, use that boolean on the dropdown container, ul.options-list
, using v-show
, like this:
<ul v-show="isOpen"
class="options-list"
>
Now we get to the events part. We need to register three events on the input element – @input
, @keyup.esc
, and @blur
.
<input
class="input is-large"
placeholder="Search..."
@input="onInput($event.target.value)"
@keyup.esc="isOpen = false"
@blur="isOpen = false"
>
For @keyup
and @blur
, everything is ready. But for @input
, we have to define a new method called onInput
accepting the value of the input.
methods: {
onInput (value) {
this.isOpen = !!value
}
}
What that method does is check whether the entered text, after casting it to boolean, is empty or not. And using that value we close or open the dropdown.
Our next task is to let users select options using the keyboard's arrow keys.
We first need to keep track of the position of the highlighted option. Thus, add highlightedPosition
to component's data list.
data () {
return {
isOpen: false,
highlightedPosition: 0
}
}
We're going to use a dynamic class, named .highlighted
, to show the currently highlighted option. And we decide whether the class should be applied to the option by comparing the option's index with highlightedPosition
.
So, modify the option element in the component like this:
<li v-for="(option, index) in options"
:class="{
'highlighted': index === highlightedPosition
}"
>
Now we've dynamically styled the currently highlighted option, let's add the two keyboard events to move selection onto other options. We'll add them on the input, along with the other events.
<input
class="input is-large"
placeholder="Search..."
@input="onInput($event.target.value)"
@keyup.esc="isOpen = false"
@blur="isOpen = false"
@keydown.down="moveDown"
@keydown.up="moveUp"
>
In this case, we prefer to use @keydown
over @keyup
to allow for continuous event firing while the key is being pressed. In other words, the selection would keep moving up/down as long as the corresponding arrow key is pressed.
Next, implement the two handlers, like so:
moveDown () {
if (!this.isOpen) {
return
}
this.highlightedPosition =
(this.highlightedPosition + 1) % this.options.length
},
moveUp () {
if (!this.isOpen) {
return
}
this.highlightedPosition = this.highlightedPosition - 1 < 0
? this.options.length - 1
: this.highlightedPosition - 1
}
So far, the user can highlight options, but can't choose them. This is going to be our next step.
Let's start by adding @keydown
event, for the enter key, on our input. This will make our input look like this:
<input
v-model="keyword"
class="input is-large"
placeholder="Search..."
@input="onInput($event.target.value)"
@keyup.esc="isOpen = false"
@blur="isOpen = false"
@keydown.down="moveDown"
@keydown.up="moveUp"
@keydown.enter="select"
>
When the user selects an option, the component should fetch the option from options list using highlightedPosition
, fill the input with the full title of the selected option, close the dropdown, and fire an event letting the parent know about it.
Here's how its implementation should look like:
select () {
const selectedOption = this.options[this.highlightedPosition]
this.keyword = selectedOption.title
this.isOpen = false
this.$emit('select', selectedOption)
}
Everything should work fine except for that this.keyword
line, because we haven't declared that variable yet.
So, add it with empty default value:
data () {
return {
isOpen: false,
highlightedPosition: 0,
keyword: ''
}
}
Then, add a two-way binding for that value using v-model
on the text input.
<input
v-model="keyword"
<!-- ... -->
>
Before you move on to the next section, you may want to check if the custom event, select
, work as expected. You can do so, by listening to it from the parent and log what gets selected.
<autocomplete-input
:options="options"
@select="onOptionSelect"
>
Log the selected option like so:
methods: {
onOptionSelect (option) {
console.log('Selected option:', option)
}
}
So far, we've been using the keyboard to interact with our component. The mouse is, of course, not less important; so, let's support it. And the great thing is that we can do so with, literally, two lines of code!
All you have to do is change the highlightedPosition
when the mouse hovers over an option and call the select
method when it clicks on it.
<li v-for="(option, index) in options"
:class="{
'highlighted': index === highlightedPosition
}"
@mouseenter="highlightedPosition = index"
@mousedown="select"
>
One important note to make here is that we used @mousedown
instead of @click
. The reason for that is because @mousedown
gets fired before @blur
, so we make sure that the option is selected before the dropdown is closed when the input is blurred.
Finally, let's make our component a real autocomplete input by filtering the options according to what the user has typed in.
Doing so only requires two small steps. First, add this computed property:
computed: {
fOptions () {
const re = new RegExp(this.keyword, 'i')
return this.options.filter(o => o.title.match(re))
}
}
After that, replace all occurrences of options
with fOptions
. To make it easier on you, here are the places you need to replace:
<li v-for="(option, index) in options"
.moveDown
, moveUp
, and select
methods.With that being done, your input should filter the displayed options properly.
One last small thing to do, to improve our component, is reset the highlighted position each time the user changes the text in the input. And you can do so, simply, by adding this.highlightedPosition = 0
in onInput
method:
onInput (value) {
this.isOpen = !!value
this.highlightedPosition = 0
}
I have to say it. I have never seen any easier way to create a reusable piece of functionality than it is with Vue. And the more time passes, the easier it becomes – like that scoped slot feature.
I hope this two-part series has taught you something new. And as always, I'm more than happy to hear your feedback and answer your questions in the comments section.
I'm a freelance web developer. Laravel & VueJS are my main tools these days and I love building stuff using them. I write constantly on this blog to share my knowledge and thoughts on things related to web development... Let's be friends on twitter.