Blog author's portrait
Artur Tagisow
Vue dev from Eastern Poland

Vue: Why is my <input> displaying something else than the :value prop I’m passing to it? Controlled inputs

Mon 17 May 2021 05:33:44 PM UTC

I’ve made an interactive CodeSandbox for this here

Boilerplate code

What we’re about to do, is write something called a “controlled input” in Vue.

Input component

<!-- FILENAME: InputNoNumbers.vue -->
<template>
  <input :value="value" @input="handleInput($event.target.value)">
</template>

<script>
export default {
  props: ['value'],
  methods: {
    handleInput(newValue) {
      const newValueWithoutNumbers = newValue.replaceAll(/[0-9]/g, '')
      this.$emit('input', newValueWithoutNumbers)
    }
  }
}
</script>

Using the input component from above

Could look like this:

<!-- FILENAME: App.vue -->
<template>
  <InputNoNumbers v-model="message"/>
</template>
<script>
import InputNoNumbers from './InputNoNumbers.vue'
export default {
  components: { InputNoNumbers },
  data() {
    return { message: 'hello' }
  }
}
</script>

The problem

If you type a string with numbers eg. "hello123" into the InputNoNumber.vue input, you’ll notice that:

This … is weird???? If:

THEN HOW IS THE INPUT DISPLAYING "hello123" IF WE’RE TELLING IT TO DISPLAY "hello"!?!?

The explanation

I’m not sure but:
Think about what events are being emitted when you write hello123a into the input.

no. keyboard input event explanation
1 hello this.$emit(‘input’, ‘hello’)
2 hello1 this.$emit(‘input’, ‘hello’) we filtered out the numbers
3 hello12 this.$emit(‘input’, ‘hello’) we filtered out the numbers
4 hello123 this.$emit(‘input’, ‘hello’) we filtered out the numbers
5 hello123a this.$emit(‘input’, ‘helloa’) we filtered out the numbers

Notice that between row number 1 and 2 the emitted value didn’t change. So the value of this.message of App.vue didn’t change.

Get it now? Vue is being lazy.
Vue only triggers a re-render/setting of display input value if a data property changes.
You write "hello", then "hello1", yet the value of this.message is the same, so no data property changed.
So Vue sleeps, thinking:

How can there be anything to render if no data property changed? zzzzzz….

BUT when you go from hello123 to hello123a, the value of the this.message data property is updated to helloa (row 5 in the table). ONLY NOW Vue decides to re-render the input and actually sync the displayed value with what is actually passed in the :value prop. Only now illegal 123 characters are removed from the input display value. Only now Vue performs input.value = this.value internally

The solutions

Since automatic re-render doesn’t trigger, you need to re-render the input manually. There are several solutions:

Solution 1 - Use this.$forceUpdate() (dumb and smart version)

<template>
  <input 
    ref="input" 
    :value="value" 
    @input="handleInput($event.target.value)"
  >
</template>

<script>
export default {
  methods: {
    handleInput(newValue) {
      const newValueWithoutNumbers = newValue.replaceAll(/[0-9]/g, '')
      // dumb version
      this.$forceUpdate()
      // smart (?) version
      // only re-render when necessary
      // if(value === newValueWithoutNumbers) {
      //   this.forceUpdate();
      // } 
      this.$emit('input', newValueWithoutNumbers)
    }
  }
}
</script>

It’s nice because it’s just one line of code. No additional code paths or logic.

The above code is for Vue 2. To $forceUpdate in Vue 3, do import { getCurrentInstance } from 'vue' then instance?.proxy?.$forceUpdate() - which is equivalent to this.$forceUpdate()

Solution 2 - set the input’s value manually before emitting it with this.$refs as Posva says here

<template>
  <input 
    ref="input" 
    :value="value" 
    @input="handleInput($event.target.value)"
  >
</template>

<script>
export default {
  methods: {
    handleInput(newValue) {
      const newValueWithoutNumbers = newValue.replaceAll(/[0-9]/g, '')
      this.$refs.input.value = newValueWithoutNumbers;
      this.$emit('input', newValueWithoutNumbers)
    }
  }
}
</script>

This is different because it shows intent as to what you’re trying to do - contrary to $forceReload which is mysterious

Further reading