Vue: Why is my input displaying something other than the :value prop I’m passing to it? Controlled inputs
Whenever you try to do some JS magic on the default HTML <input> in Vue - especially input sanitization - you’ll often come across an issue where the input will display something different than you passed to it using the :value prop. In this article I describe why that happens.
I’ve made an interactive CodeSandbox for this here
The boilerplate code
- You’re writing a custom inputbox component
- It filters out certain characters from user input
- It probably looks something like below. 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:
- the
messagedata property will have the number-free value"hello"… - …yet the text displayed in the input display will be
"hello123"
This … is weird???? If:
this.messageis"hello"v-modelputs the the value ofthis.messageintoInputNoNumber’svalueprop- and
valueis passed like<input :value"value">
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 |
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 Vue 2’s
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
- Sasha’s (aka suXin) post - has many more links all around the internet and made me realize what we’re doing in this post is called “controlled input”