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
<!-- 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>
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>
If you type a string with numbers eg. "hello123" into the
InputNoNumber.vue input, you'll notice that:
message data property will have the number-free value "hello" …"hello123"This … is weird???? If:
this.message is "hello"v-model puts the the value of this.message into InputNoNumber's
value propvalue is passed like <input :value"value">THEN HOW IS THE INPUT DISPLAYING "hello123" IF WE'RE TELLING IT TO
DISPLAY "hello"!?!?
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
Since automatic re-render doesn't trigger, you need to re-render the input manually. There are several solutions:
<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()
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