I have a Vue 2 application that uses an array of objects to back a search/multiselect widget provided by vue-multiselect.
I have looked at the Vue 1 -> 2 migration guide on debouncing calls, but the example they give did not propagate the arguments from the DOM elements to the business logic.
Right now the select fires change events with every keystroke, but I would like to throttle this (EG with lodash#throttle) so I’m not hitting my API every few milliseconds while they’re typing.
import {mapGetters} from 'vuex'; import { throttle } from 'lodash'; import Multiselect from 'vue-multiselect' export default { components: { Multiselect }, data() { return { selectedWork: {}, works: [], isLoading: false } }, computed: { ...mapGetters(['worksList']), }, methods: { getWorksAsync: throttle((term) => { // the plan is to replace this with an API call this.works = this.worksList.filter(work => titleMatches(work, term)); }, 200) } }
Problem: when the user types in the select box, I get the error:
TypeError: Cannot read property 'filter' of undefined
which is happening because this.worksList
is undefined
inside the throttle
function.
Curiously, when I use the dev tools debugger, this.worksList
has the value I need to dereference, with this
referring to the Vue component.
Currently I am not calling the API from within the component, but the problem remains the same:
- How can I throttle this call, and have the proper
this
context to update mythis.works
list? EDIT: this is explained in Vue Watch doesnt Get triggered when using axios - I also need to capture the user’s query string from the multiselect widget to pass to the API call.
What is the proper pattern in Vue 2?
Advertisement
Answer
I was unable to find an answer on SO (or anywhere) for this, but I eventually cobbled it together through trial and error, and from related materials here and in the docs.
Things that work that I didn’t do, and why
It is possible to get get the value directly using a JavaScript DOM query, and it is also possible to dig in to the multiselect component’s structure and get the value. The first solution circumvents the framework, the second depends on undocumented attributes of the multiselect component. I am avoiding both of those solutions as non-idiomatic and brittle.
My current solution
- Updated an attribute on the component whenever there was a change event in the search box. This allowed me to capture the user’s query string.
- Called my throttled async function from inside the event listener.
- Passed a regular
function
instead of an arrow function tothrottle
, which gave the correctthis
(the Vue component.)
If anyone has a suggestion for a better way to do this in Vue 2, I’m all ears.
Here’s what my solution looked like in the end:
<template> <div> <label class="typo__label" for="ajax">Async select</label> <multiselect id="ajax" v-model="selectedWork" label="title" track-by="id" placeholder="Type to search" :options="works" :searchable="true" :loading="isLoading" :internal-search="false" :multiple="false" :clear-on-select="true" :close-on-select="true" :options-limit="300" :limit="3" :limit-text="limitText" :max-height="600" :show-no-results="false" open-direction="bottom" @select="redirect" @search-change="updateSearchTerm"> <span slot="noResult">Oops! No elements found. Consider changing the search query.</span> </multiselect> </div> </template> <script> import {mapGetters} from 'vuex'; import { throttle } from 'lodash'; import Multiselect from 'vue-multiselect' export default { components: { Multiselect }, data() { return { searchTerm: '', selectedWork: {}, works: [], isLoading: false } }, computed: { ...mapGetters(['worksList']), }, methods: { limitText(count) { return `and ${count} other works`; }, redirect(work) { // redirect to selected page }, updateSearchTerm(term){ this.searchTerm = term; this.isLoading = true; this.getWorksAsync(); }, getWorksAsync: throttle(function() { const term = this.searchTerm.toLowerCase(); callMyAPI(term) .then(results => { this.works = results; this.isLoading = false; }) }, 200) } } </script>