Blog - Artur Tagisow
17 May 2021

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 of this.message into InputNoNumber’s value 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”