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
1. 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.
1.1. 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>
1.2. 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>
2. The problem
If you type a string with numbers eg. "hello123"
into the
InputNoNumber.vue
input, you’ll notice that:
- the
message
data property will have the number-free value"hello"
… - …yet the text displayed in the input display will be
"hello123"
This … is weird???? If:
this.message
is"hello"
v-model
puts the the value ofthis.message
intoInputNoNumber
’svalue
prop- and
value
is passed like<input :value"value">
THEN HOW IS THE INPUT DISPLAYING "hello123"
IF WE’RE TELLING IT TO
DISPLAY "hello"
!?!?
3. 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
4. The solutions
Since automatic re-render doesn’t trigger, you need to re-render the input manually. There are several solutions:
4.1. 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()
4.2. 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
5. 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”