Search code examples
javascriptvue.jsvuejs3

How to handle watch() with @input in vue


I want to handle a v-model change based on an @input variable update, but I'm facing issues with the order of "happenings" in vue. Here is a simplified version of my issue:

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

const foo = ref(1)
const bar = ref(1)
const lorem = ref(false)

const handleInput = (val) => {
  bar.value = val
}

watch(foo, (newFoo) => {
  if (bar.value === 2) {
    lorem.value = true
  }
})
</script>

<template>
  <input v-model="foo" @input="handleInput(2)"><br>
  {{foo}}<br>
  {{bar}}<br>
  {{lorem}}
</template>

It seems, that the watcher hits before the handleInput method was executed.

If I change the value of foo in the input field, the lorem value will be set to true if I change the value of foo a second time, only. I want to set it to true immediately.

I tried to implement nextTick() and flush: 'post', but it does not have the expected result:

<script setup>
import { ref, nextTick, watch } from 'vue'

const foo = ref(1)
const bar = ref(1)
const lorem = ref(false)

const handleInput = (val) => {
  nextTick(() => {
    bar.value = val
  })
}

watch(foo, async (newFoo) => {
  await nextTick()
  if (bar.value === 2) {
    lorem.value = true
  }
}, { flush: 'post' })
</script>

<template>
  <input v-model="foo" @input="handleInput(2)"><br>
  {{foo}}<br>
  {{bar}}<br>
  {{lorem}}
</template>

Here is a Vue Playground showing my issue.


Solution

  • In the Vue's code there are "jobs" executed seemingly as microtasks and the DOM update following.

    nextTick():

    A utility for waiting for the next DOM update flush.

    In our case the changing of an input value doesn't queue any DOM update so nextTick() is scheduled immediately as a microtask. Using 2 await nextTick(); also doesn't help... Neither {flush:post}...

    So the solution is to use more a radical way to postpone the watch's code, setTimeout() comes to mind, but I've tried requestAnimationFrame and it works, I like it better because it allows execution right after all microtasks and before the page repaint.

    Playground

    But actually you don't need this trick, just place all of your logic inside handleInput:

    Playground

    <script setup>
    
    import { ref, watch , reactive, nextTick} from 'vue'
    
    const foo = ref(1)
    const bar = ref(1)
    const lorem = ref(false)
    const log = reactive([]);
    
    const handleInput = (val) => {
      bar.value = val
      lorem.value = bar.value === 2;
      log.push(`bar: ${val} lorem: ${lorem.value} foo: ${foo.value}`);
    }
    
    </script>
    
    <template>
      <input v-model="foo" @input="handleInput(2)"><br>
      <input v-model="foo" @input="handleInput(1)"><br>
      {{foo}}<br>
      {{bar}}<br>
      {{lorem}}
      <div v-for="line in log">
        {{line}}
      </div>
    </template>