Tutorial

How To Build an Autocomplete Component with Vue.js

Updated on March 18, 2021
author

Filipa Lacerda

How To Build an Autocomplete Component with Vue.js

Introduction

Autocompletion is a common modern feature. When a user interacts with an input field, they are provided with a list of suggested values that are relevant to what they typed in.

In this article, you will learn how to make an autocomplete component using v-model, how to handle events with key modifiers, and how to prepare it for async requests.

Prerequisites

To complete this tutorial, you will need:

This tutorial was verified with Node v15.3.0, npm v6.14.9, and vue v2.6.11.

Step 1 — Setting Up the Project

For the purpose of this tutorial, you will build from a default Vue project generated with @vue/cli.

  1. npx @vue/cli create vue-autocomplete-component-example --default

This will configure a new Vue project with default configurations: Vue 2, babel, eslint.

Navigate to the newly created project directory:

  1. cd vue-autocomplete-component-example

Now, you can use your code editor to create a new autocomplete component. This will be a single-file Vue component with a template, scripts, and styles.

In order to build an autocomplete component, your template will need at least two things: an input and a list.

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      type="text"
    />
    <ul
      class="autocomplete-results"
    >
      <li
        class="autocomplete-result"
      >
        (result)
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete'
};
</script>

This code will create a div with an input for entering text and an unordered list for displaying autocomplete results.

Next, provide some styling for the component in the same file:

src/components/SearchAutocomplete.vue
<style>
  .autocomplete {
    position: relative;
  }

  .autocomplete-results {
    padding: 0;
    margin: 0;
    border: 1px solid #eeeeee;
    height: 120px;
    min-height: 1em;
    max-height: 6em;    
    overflow: auto;
  }

  .autocomplete-result {
    list-style: none;
    text-align: left;
    padding: 4px 2px;
    cursor: pointer;
  }

  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

Step 2 — Filtering the Search Results

When the user types, you will want to show them a list of results. Your code will need to listen for input changes to know when to show those results.

In order to do so you will make use of v-model. v-model is a directive with two way data-binding for form inputs and textareas that updates the data on user input events.

Because you will need to know when the user has finished typing to filter the results, you will also add an event listener for @input.

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      class="autocomplete-results"
    >
      <li
        class="autocomplete-result"
      >
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  data() {
    return {
      search: '',
    };
  },
  methods: {
    onChange() {
      // ...
    }
  }
};
</script>

Now that you know what to search for and when to do it, you will need some data to show.

For the purposes of this tutorial, you will use an array of values, but you can update the filter function to handle more complex data structures.

Open App.vue and modify it to import and reference the autocomplete component:

src/App.vue
<template>
  <div id="app">
    <SearchAutocomplete
      :items="[
        'Apple',
        'Banana',
        'Orange',
        'Mango',
        'Pear',
        'Peach',
        'Grape',
        'Tangerine',
        'Pineapple'
      ]"
    />
  </div>
</template>

<script>
import SearchAutocomplete from './components/SearchAutocomplete.vue'

export default {
  name: 'App',
  components: {
    SearchAutocomplete
  }
}
</script>

Next, revisit SearchAutocomplete.vue in your code editor and add code to filter search results and display them:

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        class="autocomplete-result"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  props: {
    items: {
      type: Array,
      required: false,
      default: () => [],
    },
  },
  data() {
    return {
      search: '',
      results: [],
      isOpen: false,
    };
  },
  methods: {
    filterResults() {
      this.results = this.items.filter(item => item.toLowerCase().indexOf(this.search.toLowerCase()) > -1);
    },
    onChange() {
      this.filterResults();
      this.isOpen = true;
    }
  },
}
</script>

In filterResults(), notice how toLowerCase() is applied to both the typed text and each element of the array. This ensures that a user can either use capital or lowercase words and still get relevant results.

You also need to make sure that you only show the list of results after the user has typed something. You can accomplish that by conditionally displaying it with the usage of v-show.

Note: The reason to use v-show instead of v-if is that the visibility of this list will often be toggled and although the initial render cost of v-show is higher, v-if has higher toggle costs.

Step 3 — Updating Search Results with Click Events

Now you will need to make sure the list of results is usable. You will want users to be able to click on one of the results and automatically show that value as the chosen one.

You can accomplish that by listening to the click event and setting the value as the search term:

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

You will also need to close the list of results once that happens for a better user experience:

src/components/SearchAutocomplete.vue
<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  methods: {
    setResult(result) {
      this.search = result;
      this.isOpen = false;
    },
    // ...
  },
}
</script>

To close the list of results once the user clicks off the results list you will need to listen to a click event outside this component. Let’s accomplish that once the component is mounted and when the user clicks somewhere you will need to check if it was outside of the component. You will use Node.contains to check if the event target belongs to the component.

src/components/SearchAutocomplete.vue
<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  mounted() {
    document.addEventListener('click', this.handleClickOutside);
  },
  destroyed() {
    document.removeEventListener('click', this.handleClickOutside);
  }
  methods: {
    // ...
    handleClickOutside(event) {
      if (!this.$el.contains(event.target)) {
        this.isOpen = false;
      }
    }
  },
}
</script>

At this point, you can compile your application:

  1. npm run serve

Then, open it in a web browser. You will be able to interact with the autocomplete component and ensure that it suggests items from the array.

Step 4 — Supporting Arrow Key Navigation

Let’s add an event listener for both UP, DOWN and ENTER keys. Thanks to key modifiers, Vue provides aliases for the most commonly used keys, so you will not need to verify which keycode belongs to each key.

In order to track which result is being selected, you will add an arrowCounter to hold the value for the current index in the search results array. Set the initial value of arrowCounter to -1 to guarantee that no option is selected before the user actively selects one. When the user presses the DOWN key, increase the arrowCounter value by 1. When the user presses the UP key, decrease the arrowCounter value by 1.

When the user presses the ENTER key, you will need to get that index from the array of results.

You will need to be careful to not keep counting once the list ends and not to start counting before the results are visible.

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  // ...
  data() {
    return {
      search: '',
      results: [],
      isOpen: false,
      arrowCounter: -1
    };
  },
  methods: {
    // ...
    handleClickOutside(event) {
      if (!this.$el.contains(event.target)) {
        this.arrowCounter = -1;
        this.isOpen = false;
      }
    },
    onArrowDown() {
      if (this.arrowCounter < this.results.length) {
        this.arrowCounter = this.arrowCounter + 1;
      }
    },
    onArrowUp() {
      if (this.arrowCounter > 0) {
        this.arrowCounter = this.arrowCounter - 1;
      }
    },
    onEnter() {
      this.search = this.results[this.arrowCounter];
      this.arrowCounter = -1;
      this.isOpen = false;
    }
  },
}
</script>

Let’s also add a visual aid by adding an active CSS class to the selected option:

src/components/SearchAutocomplete.vue
<style>
  // ...

  .autocomplete-result.is-active,
  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

Step 5 — Handling Async Loading

You can additionally provide async support by informing the component that it needs to wait for a response from the server to load the results.

Note: You could make the request inside the component, but most apps already use a specific lib to make requests, no need to add a dependency here.

You will need to make a couple of changes to the component:

  1. A pointer to inform whether we need to wait or not for the results
  2. Emit an event to the parent component once the value of the input changes
  3. A watcher to know when the data is received
  4. A loading indicator to inform the user
src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      v-model="search"
      @input="onChange"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
      type="text"
    />
    <ul
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        v-if="isLoading"
        class="loading"
      >
        Loading results...
      </li>
      <li
        v-else
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'SearchAutocomplete',
  props: {
    // ...
    isAsync: {
      type: Boolean
      required: false,
      default: false,
    },
  },
  // ...
  watch: {
    items: function (value, oldValue) {
      if (this.isAsync) {
        this.results = value;
        this.isOpen = true;
        this.isLoading = false;
      },
    }
  },
  // ...
  methods: {
    // ...
    onChange() {
      this.$emit('input', this.search);

      if (this.isAsync) {
        this.isLoading = true;
      } else {
        this.filterResults();
        this.isOpen = true;
      }
    },
    // ...
  },
}
</script>

This code change introduces an isAsync prop. If this is present in the component will filter the results by comparing items that are provided by a request from a server request.

Step 6 — Wrapping Up the Project

With all the changes applied, SearchAutocomplete.vue will resemble the following:

src/components/SearchAutocomplete.vue
<template>
  <div class="autocomplete">
    <input
      type="text"
      @input="onChange"
      v-model="search"
      @keydown.down="onArrowDown"
      @keydown.up="onArrowUp"
      @keydown.enter="onEnter"
    />
    <ul
      id="autocomplete-results"
      v-show="isOpen"
      class="autocomplete-results"
    >
      <li
        class="loading"
        v-if="isLoading"
      >
        Loading results...
      </li>
      <li
        v-else
        v-for="(result, i) in results"
        :key="i"
        @click="setResult(result)"
        class="autocomplete-result"
        :class="{ 'is-active': i === arrowCounter }"
      >
        {{ result }}
      </li>
    </ul>
  </div>
</template>

<script>
  export default {
    name: 'SearchAutocomplete',
    props: {
      items: {
        type: Array,
        required: false,
        default: () => [],
      },
      isAsync: {
        type: Boolean,
        required: false,
        default: false,
      },
    },
    data() {
      return {
        isOpen: false,
        results: [],
        search: '',
        isLoading: false,
        arrowCounter: -1,
      };
    },
    watch: {
      items: function (value, oldValue) {
        if (value.length !== oldValue.length) {
          this.results = value;
          this.isLoading = false;
        }
      },
    },
    mounted() {
      document.addEventListener('click', this.handleClickOutside)
    },
    destroyed() {
      document.removeEventListener('click', this.handleClickOutside)
    },
    methods: {
      setResult(result) {
        this.search = result;
        this.isOpen = false;
      },
      filterResults() {
        this.results = this.items.filter((item) => {
          return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
        });
      },
      onChange() {
        this.$emit('input', this.search);

        if (this.isAsync) {
          this.isLoading = true;
        } else {
          this.filterResults();
          this.isOpen = true;
        }
      },
      handleClickOutside(event) {
        if (!this.$el.contains(event.target)) {
          this.isOpen = false;
          this.arrowCounter = -1;
        }
      },
      onArrowDown() {
        if (this.arrowCounter < this.results.length) {
          this.arrowCounter = this.arrowCounter + 1;
        }
      },
      onArrowUp() {
        if (this.arrowCounter > 0) {
          this.arrowCounter = this.arrowCounter - 1;
        }
      },
      onEnter() {
        this.search = this.results[this.arrowCounter];
        this.isOpen = false;
        this.arrowCounter = -1;
      },
    },
  };
</script>

<style>
  .autocomplete {
    position: relative;
  }

  .autocomplete-results {
    padding: 0;
    margin: 0;
    border: 1px solid #eeeeee;
    height: 120px;
    overflow: auto;
  }

  .autocomplete-result {
    list-style: none;
    text-align: left;
    padding: 4px 2px;
    cursor: pointer;
  }

  .autocomplete-result.is-active,
  .autocomplete-result:hover {
    background-color: #4AAE9B;
    color: white;
  }
</style>

A live demo is available via CodePen.

Conclusion

In this article, you constructed an autocomplete component with Vue.js. This was achieved by utilizing v-model, filter, @input, @click, @keydown, and .$emit.

If you’d like to learn more about Vue.js, check out our Vue.js topic page for exercises and programming projects.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Filipa Lacerda

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
3 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Hi, nice tutorial. But one thing you can implement here. If I want to cycle around the list items. For example if the length of the items are 6 and I press the down button after selection of the 6th element it should cycle back to the 1st item. Again if I wan to press the up button when the 1st item is selected it should be cycle down to 6th element. I have successfully did it. Hope it should work.

Here I made the arrowCounter to 0 by default in the data method:

// ...
onArrowDown() {
  if(this.arrowCounter < this.results.length - 1) {
    this.arrowCounter = this.arrowCounter + 1;
  } else {
    this.arrowCounter = 0;
  }
},
onArrowDown() {
  if(this.arrowCounter >= 1) {
    this.arrowCounter = this.arrowCounter - 1;
  } else {
    this.arrowCounter = this.results.length - 1;
  }
}
//...

Hello, great tutorial! What if the data comes from an API? Could you do another tutorial about such a variation as well?

Great walkthrough, thank you for taking the time to put this together. One tiny but important thing that needs implementation however is how to emit the user’s selection (or end result) to the parent component. Right now only the text you enter is being emitted.

To make this happen make this small change to the setResult method:

methods: {
      setResult(result) {
        this.search = result;
        this.isOpen = false;
        this.$emit('selected', result)
      }

Then in your parent component use v-on to bind to the emitted event:

<autocomplete
    :items="items"
    @selected="item = $event"
 ></autocomplete>

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more