In this part of our ongoing series about Building reusable custom components with Vue.js we are going to build an autocomplete dropdown from scratch.

It’s reasonable to ask why we don’t just use one of the many existing auto complete components available for Vue.js.

For us it came down to:

  1. Not finding an existing component that behaved exactly how we wanted
  2. Trying to keep our dependency footprint small
  3. Wanting to focus on simple UI interaction first and circle back around to enhanced functionality later

Keep in mind, however, the lesson here is not that you should build all of your autocompletes from scratch. Rather, I want to show you how to build any kind of component in a way that allows you to enhance its functionality over time and ensures that you can reuse your components across your application.

Getting Started

Here’s what we are going to be creating:

We’ll start by copying the contents of Dropdown.vue into a new component AutocompleteDropdown.vue. Then we can load it in our Filters.vue component:

<div class="filter">
  <label for="autocomplete-dropdown">Autocomplete dropdown: </label>
  <autocomplete-dropdown id="autocomplete-dropdown" :options="fruitOptions" v-model="selectedFruit"></autocomplete-dropdown>
</div>
import Dropdown from '@/components/Dropdown'
import AutocompleteDropdown from '@/components/AutocompleteDropdown'
 
export default {
  components: {
    'dropdown': Dropdown,
    'autocomplete-dropdown': AutocompleteDropdown
  },
}

Autocomplete field

Text field

Our first step is to remove the HTML select element. Our final component will not use a standard dropdown. Instead we will allow the user to type their choice into a text box. As they type, results which match the available options will be displayed in a list below the text box.

We don’t want to directly set the selected option to whatever is typed into the text field. Rather we want to use what is typed as a query to search against the available options. So, we will v-model the input’s value to a new data property in our component called searchText:

<template>
  <div class="dropdown">
    <input type="text" v-model="searchText"></input>
  </div>
</template>
<script>
  data () {
    return {
      searchText: ''
    }
  }
</script>

We can also remove our mounted handler and watch handlers as we will be reimplementing that behavior.

Matches

Next, we need a way for the component to determine which, if any, of the available options match the supplied search text.

We can do this with a computed property that filters an array of options based on some criteria.

computed: {
  matches () {
    return Object.entries(this.options).filter((option) => {
      var optionText = option[0].toUpperCase()
      return optionText.match(this.searchText.toUpperCase())
    })
  }
},

Let’s break this down piece by piece.

Recall from Part 2 that our options are specified as an object with string keys. This means that we can’t call filter on it directly. Instead we convert the key => value properties into an array of [key, value] arrays using Object.entries.

Note: not all browsers support Object.entries. If you need to, you can use a shim or just create the array yourself:

computed: {
  matches () {
    var optionArray = []
    for (var key in this.options) {
      if (has(this.options, key) && isEnumerable(this.options, key)) {
          optionArray.push([key, this.options[key]]);
      }
    }
 
    return optionArray.filter((option) => {
      var optionText = option[0].toUpperCase()
      return optionText.match(this.searchText.toUpperCase())
    })
  }
},

Next, we filter the array of options with a custom function. That function takes the first element of the option array, which is the text of the option, and converts it to uppercase. We then perform a direct match against the uppercase searchText which is provided by our text input.

Note: you can make the autocomplete case-sensitive by removing both of the calls to toUppercase().

At this point we can test our matching behavior by adding a template tag for matches:

<template>
  <div class="dropdown">
    <input type="text" v-model="searchText"></input>
    <pre>{{ matches }}</pre>
  </div>
</template>

Try typing into the text field, you should see the set of computed matches change as you type.

Next we need to implement the completion suggestion dropdown.

The dropdown suggestion list

We will render this in the template as a ul with one or more list items representing the matched items:

<ul class="suggestion-list">
  <li v-for="(suggestion, index) in matches">
    {{ suggestion[0] }}
  </li>
</ul>

and add some accompanying styles which position our list below the input:

.dropdown {
  display: inline-block;
  position: relative;
}
 
.suggestion-list {
  background-color: rgba(255, 255, 255, 0.95);
  border: 1px solid #ddd;
  list-style: none;
  display: block;
  margin: 0;
  padding: 0;
  width: 100%;
  overflow: hidden;
  position: absolute;
  top: 20px;
  left: 0;
  z-index: 2;
}

Note: you can remove the ‘matches’ tag we added in the last step now.

Open/close behavior

CSS class

We will now introduce the concept of our suggestion dropdown being “open” or “closed”. We’ll track this with a data property and a class on the dropdown’s parent component:

data () {
  return {
    searchText: '',
    selectedOption: null,
    open: false
  }
},
Next Post in Series

Comments

Permalink

Really great post, I’m trying to understand how to build reusable components fully and wanted to know if you heather full code was available for this?

Permalink

Brilliant series which brings together so many Vue concepts for newcomers, all together in one place.

Just to complete the picture, would be great to see where options (fruit) have other attributes e.g. string: colour; or array: countries of origin, how to display those particular attributes for a selected fruit in text boxes or lists near the dropdowns.

For the advanced class, choosing the selection criteria from the available attributes - (fruit) name, colour or country of origin - still re-using the same components for the basic or auto -complete options.

Thanks again, Jay

Christchurch, Dorset UK

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.