Building an Awesome Reusable Autocomplete-Input Component in Vue 2.1 (Part Two)

Building an Awesome Reusable Autocomplete-Input Component in Vue 2.1 (Part Two)

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.

Closing or opening the options dropdown

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.

Highlighting options with keyboard's up/down arrow keys

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
}

Selecting an option

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)
  }
}

Support mouse interactions

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.

Enabling filtering

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
}

In closing

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.

Vue
Taha Shashtari

About Taha Shashtari

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.