Dynamic native browser autocompletion with JavaScript and Vue 3

Revision history
Tags: html, javascript, vue

Preface

Some time ago, I noticed that it was possible to load a predefined set of autocomplete options for HTML <input type="search"> using the list attribute. By referencing a <datalist> element by its id we can choose what the browser will suggest for autocompletion while typing.

<input type="text" list="myDataList">
<datalist id="myDataList">
  <option value="suggestion 1">
  <option value="suggestion 2">
</datalist>

I wanted to see if this list could be updated dynamically while typing. This can definitely be done with vanilla JavaScript, but since I’m interested in getting to know Vue 3 better, I’m using that. The concepts, however, will still be useful for those who wants to go vanilla or with other frameworks.

Setup

Input field

For Vue we need a container for our app, so that we can control what’s inside of it. I’m going to use a <form>. This container is given an id so that we can reference it when we configure the Vue app later on.

For this example I will be simulating a search field. An <input> element with type="search" is a very semantically correct choice.

<form id="app">
  <input type="search" list="searchSuggestions" autocomplete="off" @input="onchange">
</form>

Datalist

The <datalist> will hold the autocomplete suggestions. Using v-for here will iterate over all the items in the suggestions array in our Vue app instance so that an <option> element will be created for each item. A single <option> represents a single suggestion.

<datalist id="searchSuggestions">
  <!-- Render an option element for each suggested word -->
  <option v-for="s in suggestions" :value="s">
</datalist>

Vue.js app

I’ll let this code and the included comments speak for themselves.

Vue.createApp({
  // Sets up the intial state
  data: function() {
    return {
      suggestions: []
    }
  },
  // App instance methods
  methods: {
    // Returns matching words for the provided query string.
    // This method should probably go fetch something from the database
    // when used in the real world.
    getMatchingWords(s) {
      const database = ["foo", "bar", "baz", "fourty two", "anaheim"]
      const matches = database.filter(words => words.includes(s))
      return Promise.resolve(matches)
    },
    // Updates the variable that the datalist is rendered from.
    updateSuggestions(query) {
      this.getMatchingWords(query).then(words => {
        // Must splice the existing array to avoid overwriting the
        // array Vue is observing (i.e. the one returned from data()).
        this.suggestions.splice(0, this.suggestions.length, ...words)
      })
    },
    // Fired for every input event because of the `@input` attribute
    // on the form element.
    onchange(event) {
      // Get the value of the search input
      const query = event.target.value

      // Avoid firing the update function excessively while
      // typing fast by "debouncing" the function.
      _.debounce(() => this.updateSuggestions(query), 300)()
    }
  }
}).mount('#app')

Complete solution

Below is the code. You can see it in action here.

<!DOCTYPE html>

<script src="https://unpkg.com/lodash@4.17.21/lodash.min.js"></script>
<script src="https://unpkg.com/vue@3"></script>

<form id="app">
  <datalist id="searchSuggestions">
    <!-- Render an option element for each suggested word -->
    <option v-for="s in suggestions" :value="s">
  </datalist>

  <input type="search" list="searchSuggestions" autocomplete="off" @input="onchange">
</form>

<script>
  'use strict'

  Vue.createApp({
    // Sets up the intial state
    data: function() {
      return {
        suggestions: []
      }
    },
    // App instance methods
    methods: {
      // Returns matching words for the provided query string.
      // This method should probably go fetch something from the database
      // when used in the real world.
      getMatchingWords(s) {
        const database = ["foo", "bar", "baz", "fourty two", "anaheim"]
        const matches = database.filter(words => words.includes(s))
        return Promise.resolve(matches)
      },
      // Updates the variable that the datalist is rendered from.
      updateSuggestions(query) {
        this.getMatchingWords(query).then(words => {
          // Must splice the existing array to avoid overwriting the
          // array Vue is observing (i.e. the one returned from data()).
          this.suggestions.splice(0, this.suggestions.length, ...words)
        })
      },
      // Fired for every input event because of the `@input` attribute
      // on the form element.
      onchange(event) {
        // Get the value of the search input
        const query = event.target.value

        // Avoid firing the update function excessively while
        // typing fast by "debouncing" the function.
        _.debounce(() => this.updateSuggestions(query), 300)()
      }
    }
  }).mount('#app')
</script>

References

If you have any comments or feedback, please send me an e-mail. (stig at stigok dotcom).

Did you find any typos, incorrect information, or have something to add? Then please propose a change to this post.

Creative Commons License This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.