7

I have a filter input field and want to filter a list of items. The list is large so I want to use debounce to delay the filter being applied until the user has stopped typing for improved user experience. This is my input field and it's bound to filterText that is used to filter the list.

<input type="text" v-model="state.filterText" />
Andre
  • 1,740
  • 2
  • 13
  • 15

8 Answers8

13

I didn't find any nice solution as I wanted to see my binding in my template so I decided to share my solution. I wrote a simple debounce function and use the following syntax to bind the behavior:

setup() {
...

  function createDebounce() {
    let timeout = null;
    return function (fnc, delayMs) {
      clearTimeout(timeout);
      timeout = setTimeout(() => {
        fnc();
      }, delayMs || 500);
    };
  }

  return {
    state,
    debounce: createDebounce(),
  };
},

And the template syntax:

    <input
      type="text"
      :value="state.filterText"
      @input="debounce(() => { state.filterText = $event.target.value })"
    />
ARNON
  • 1,097
  • 1
  • 15
  • 33
Andre
  • 1,740
  • 2
  • 13
  • 15
  • I would return the debounced method from `setup` so the template just does `@input="debouncedFilter"`. In setup, return `debouncedFilter: createDebounce((evt)=>{state.filterText=evt.target.value})` – danbars Jan 06 '22 at 03:27
  • Is there no built-in way in vue3 like we have in alpinejs ? `input @input.debounce` – anjanesh Jan 01 '23 at 01:59
12

Hi first time answering something here, so correct my answer as much as you want, I'd appreciate it. I think that the prettiest and lightest solution is to create a directive globally that you can use as much as you want in all of your forms.

you first create the file with your directive, eg. debouncer.js

and you create the function for the debouncing

    //debouncer.js
    /*
      This is the typical debouncer function that receives
      the "callback" and the time it will wait to emit the event
    */
    function debouncer (fn, delay) {
        var timeoutID = null
        return function () {
          clearTimeout(timeoutID)
          var args = arguments
          var that = this
          timeoutID = setTimeout(function () {
            fn.apply(that, args)
          }, delay)
        }
      }

    /*
      this function receives the element where the directive
      will be set in and also the value set in it
      if the value has changed then it will rebind the event
      it has a default timeout of 500 milliseconds
    */
    module.exports = function debounce(el, binding) {
      if(binding.value !== binding.oldValue) {
        el.oninput = debouncer(function(){
          el.dispatchEvent(new Event('change'))
        }, parseInt(binding.value) || 500)
      }
    }

After you define this file you can go to your main.js import it and use the exported function.

    //main.js
    import { createApp } from 'vue'
    import debounce from './directives/debounce' // file being imported
    
    const app = createApp(App)

    //defining the directive
    app.directive('debounce', (el,binding) => debounce(el,binding))

    app.mount('#app')

And its done, when you want to use the directive on an input you simply do it like this, no imports or anything.

    //Component.vue
    <input
       :placeholder="filter by name"
       v-model.lazy="filter.value" v-debounce="400"
    />

The v-model.lazy directive is important if you choose to do it this way, because by default it will update your binded property on the input event, but setting this will make it wait for a change event instead, which is the event we are emitting in our debounce function. Doing this will stop the v-model updating itself until you stop writing or the timeout runs out (which you can set in the value of the directive). I hope this was understandable.

Community
  • 1
  • 1
jyucranav
  • 121
  • 1
  • 3
  • 1
    Nice approach. I tried to use it, but there's a problem - When I emit `update:model-value` from a vue component, the `$event` is simply the new value, and not a js event object as it is with native html components. So when I listen to `@change` I get js event and I don't have access to the new value. Any idea how to solve this? I tried adding `debouncer(function(inputEvent){....})`, but it didn't seem to be the event emitted from the component. I think `el.oninput` needs to be replaced with the equivalence of `@update:model-value` and not the dom's event listener, but I don't know how to do it – danbars Dec 13 '21 at 04:19
8

For some easier solutions, you can use popular libraries:

  1. Using Lodash:
<template>
  <input type="text" v-model="searchText" @input="onInput" />
</template>

<script setup>
import debounce from "lodash/debounce"
const onInput = debounce(() => {
  console.log(searchText.value)
}, 500)
</script>
  1. Using vueUse:
<template>
  <input type="text" v-model="searchText" @input="onInput" />
</template>

<script setup>
import { useDebounceFn } from "@vueuse/core"
const onInput = useDebounceFn(() => {
  console.log(searchText.value)
}, 500)
</script>
KitKit
  • 8,549
  • 12
  • 56
  • 82
2
<template>
    <input type="text" :value="name" @input="test" />
    <span>{{ name }}</span>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
function debounce<T> (fn: T, wait: number) {
  let timer: ReturnType<typeof setTimeout>
  return (event: Event) => {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      if (typeof fn === 'function') {
        fn(event)
      }
    }, wait)
  }
}

export default defineComponent({
  setup () {
    const name = ref('test')
    function setInputValue (event: Event) {
      const target = event.target as HTMLInputElement
      name.value = target.value
    }
    const test = debounce(setInputValue, 1000)
    return { name, test }
  }
})
</script>
  • thanks for sharing your solution, but code only answers are not really helping. please try to improve your answer by commenting on the stuff you are doing.. – honk31 Feb 01 '23 at 12:17
2

<input @input="updateValue"/>

const updateValue = (event) => {
  const timeoutId = window.setTimeout(() => {}, 0);
  for (let id = timeoutId; id >= 0; id -= 1) {
    window.clearTimeout(id);
  }

  setTimeout(() => {
    console.log(event.target.value)
  }, 500);
};
You can try this one
2

Here's an example with Lodash and script setup syntax using a watcher to fire the debounced api call:

<script setup>
import { ref, watch } from 'vue'
import debounce from 'lodash.debounce'

const searchTerms = ref('')

const getFilteredResults = async () => {
  try {
    console.log('filter changed')
    // make axios call here using searchTerms.value
  } catch (err) {
    throw new Error(`Problem filtering results: ${err}.`)
  }
}

const debouncedFilter = debounce(getFilteredResults, 250) // 250ms delay

watch(() => searchTerms.value, debouncedFilter)    
</script>

<template>
    <input v-model="searchTerms" />
</template>
agm1984
  • 15,500
  • 6
  • 89
  • 113
1

Creating a debounced ref that only updates the value after a certain timeout after the latest set call:

import { customRef } from 'vue'

export function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

Usage in component:

<script setup>
import { useDebouncedRef } from './debouncedRef'
const text = useDebouncedRef('hello')
</script>

<template>
  <input v-model="text" />
</template>

https://vuejs.org/api/reactivity-advanced.html#customref

bad4iz
  • 220
  • 2
  • 4
0

https://www.npmjs.com/package/vue-debounce now works for vue 3 It can be registered also with composition API like this

setup() {
  ...
},
directives: {
  debounce: vue3Debounce({ lock: true })
}
DonMB
  • 2,550
  • 3
  • 28
  • 59