Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type ahead complete experience with VueJS

I'm looking to create an input field that offers suggestions on completions like what VScode "Intellisense" (I think) or like dmenu does.

I have been using Vue JS and code like:

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
    <label>Lookup German word:
        <input type="text" v-model.trim="word" v-on:keyup="signalChange" v-on:change="signalChange" list="words" autofocus>
    </label>
    <datalist id="words">
        <option v-for="w in words">${w}</option>
    </datalist>
    Query: ${query} Results: ${words.length} Time taken: ${fetchtime} ms
</div>

<script>
    const app = new Vue({
        el:'#app',
        delimiters: ['${', '}'],
        data() {
            return {
                listId:'words',
                word:'',
                query:'',
                words:[],
                fetchtime: 0
            }
        },
        methods: {
            async signalChange(){
                console.log(this.word)
                if (this.word.length > 2 && this.word.slice(0,3).toLowerCase() != this.query) {
                    this.query = this.word.slice(0,3).toLowerCase()
                    let time1 = performance.now()
                    let response = await fetch('https://dfts.dabase.com/?q=' + this.query)
                    const words = await response.json()
                    let time2 = performance.now()                    
                    this.fetchtime = time2 - time1
                    this.listId="";
                    this.words = words
                    setTimeout(()=>this.listId="words");
                }
            }
        }
    })
</script>

Where signalChange would fetch some completion results.

However the User Experience (UX) is non-intuitive. You have to backspace to see the completions after typing three characters like "for". I've tried a couple of browsers and the VueJS experience is pretty poor across the board. However it works ok without VueJS.

Is there something I am missing? Demo: https://dfts.dabase.com/

Perhaps I need to create my own dropdown HTML in VueJS like what happens in https://dl.dabase.com/?polyfill=true ?

like image 900
hendry Avatar asked Mar 14 '21 06:03

hendry


Video Answer


1 Answers

Performance issue on Chrome

There is a performance issue on Chrome reported here: Is this a Chrome UI performance bug related to input + datalist?

Applying the solution to your Vue code works fine for Chrome:

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
    <label>Lookup German word:
        <input type="text" v-model.trim="word" v-on:keyup="signalChange" v-on:change="signalChange" list="words" autofocus>
    </label>
    <datalist v-bind:id="listId">
        <option v-for="w in words">${w}</option>
    </datalist>
    Query: ${query} Results: ${words.length} Time taken: ${fetchtime} ms
</div>

<script>
    const app = new Vue({
        el:'#app',
        delimiters: ['${', '}'],
        data() {
            return {
                listId:'words',
                word:'',
                query:'',
                words:[],
                fetchtime: 0
            }
        },
        methods: {
            async signalChange(){
                console.log(this.word)
                if (this.word.length > 2 && this.word.slice(0,3).toLowerCase() != this.query) {
                    this.query = this.word.slice(0,3).toLowerCase()
                    let time1 = performance.now()
                    let response = await fetch('https://dfts.dabase.com/?q=' + this.query)
                    const words = await response.json()
                    let time2 = performance.now()                    
                    this.fetchtime = time2 - time1
                    this.listId="";
                    this.words = words
                    setTimeout(()=>this.listId="words");
                }
            }
        }
    })
</script>

Firefox still won't work properly with this, so refer to my original answer below about that:

Original answer:

I noticed a big lag when running your code, so I started fiddling a bit and it seems that the issue is generating the data-list options for a large amount of items.

Since you will be only showing a few results anyway, what can be done is to limit the amount of rendered options and then use filter to show further results when more characters are added.

This works fine on Chrome but still fails on Firefox (although there's a known issue in Firefox: https://bugzilla.mozilla.org/show_bug.cgi?id=1474137)

Check it out:

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <label>Lookup German word:
    <input type="text" v-model="word" v-on:keyup="signalChange"  list="words" autofocus>
  </label>
  <datalist id="words">
    <option v-for="w in words">${w}</option>
  </datalist> Query: ${query} Results: ${fetchedWords.length} Time taken: ${fetchtime} ms
</div>

<script>
  new Vue({
    el: '#app',
    delimiters: ['${', '}'],
    data() {
      return {
        word: '',
        query: '',
        words: [],
        fetchedWords: [],
        fetchtime: 0
      }
    },
    methods: {
      async signalChange() {
        if (this.word.length > 2 && this.word.slice(0, 3).toLowerCase() != this.query) {
          this.query = this.word.slice(0, 3).toLowerCase();
          let response = await fetch('https://dfts.dabase.com/?q=' + this.query);
          this.fetchedWords = (await response.json());
          this.words = this.fetchedWords.slice(0, 10);
        } else if (this.word.includes(this.query)) {
          this.words = this.fetchedWords.filter(w => w.startsWith(this.word)).slice(0, 10);
        } else {
          this.words = [];
        }
      }
    }


  })
</script>

Edit: Is this only a Vue related issue? No.

I created an equivalent implementation in pure JS+HTML. Used a performant way to minimize DOM creation time (created a fragment and only attach it once to the DOM as per How to populate a large datalist (~2000 items) from a dictionary) but it still takes a long time to become responsive. Once it does it works well, but on my machine it took almost a minute after inputting "was" to become responsive.

Here's the implementation in pure JS+HTML:

let word = '';
let query = '';
const input = document.querySelector('input');
const combo = document.getElementById('words');
input.onkeyup = function signalChange(e) {
  word = e.target.value;
  console.log(word)
  if (word.length > 2 && word.slice(0, 3).toLowerCase() != query) {
    query = word.slice(0, 3).toLowerCase();
    fetch('https://dfts.dabase.com/?q=' + query)
      .then(response => response.json())
      .then(words => {
        const frag = document.createDocumentFragment();
        words.forEach(w => {
          var option = document.createElement("OPTION");
          option.textContent = w;
          option.value = w;
          frag.appendChild(option);
        })
        combo.appendChild(frag);
      });
  }
}
<div id="app">
  <label>Lookup German word:
    <input type="text" list="words" autofocus>
  </label>
  <datalist id="words"></datalist>
</div>

So, taking this into account and the limited experience in firefox due to bugs you should implement a custom autocomplete without the datalist.

For a good performance, if the list is very large you may want to keep the entire list out of the DOM anyway, and update it as the user changes the input or scrolls in the list.

Here's an example of an existing custom autocomplete working with the API from the OP's example: https://jsfiddle.net/ywrvhLa8/4/

like image 131
Tiago Coelho Avatar answered Oct 22 '22 18:10

Tiago Coelho