Vue Controlled Input - using v-model and watch

May 25 2019

What I want

Vue

<template>
    <div>
        Value: <input v-model="value"/>
    </div>
</template>

<script>
export default {
    data: () => ( {
        value: ''
    } ),

    watch: {
        value: function( newValue ) {
            newValue = newValue.replace( /[^\d]/g, '' ).trim()
            if ( newValue.length > 0 ) {
                newValue = newValue + '$'
            }
            this.value = newValue
        }
    }
}

Compared to React

class ControlledInput extends React.Component {
    constructor() {
        super()
        this.state = {
            value: '',
        }

        this.onChange = this.onChange.bind(this)
    }

    onChange(e) {
        let value = e.target.value.replace(/[^\d]/g, '').trim()
        if (value.length > 0) {
            value = value + '$'
        }
        this.setState({ value: value })
    }

    render() {
        return (
            <div>
                Value: <input value={this.state.value} onChange={this.onChange} />
            </div>
        )
    }
}

About getting there

At first I was very surprised to not find anything similar to this solution. Searching Vue controlled input only gave me one blog post and some StackOverflow answers, most of them not solving the problem in a way I want, going around it or being more complex than it should.

Some websites may have an input with very strict validation: be it telephone number, amount of money, or something else that implies an input mask. Formatting the input as you type it may ease checking whatever that's typed for user. It's much easier to read 4093 8123 9332 8423 8495 rather than 40938123933284238495.

Unfortunately we don't have much options when it comes to formatting and styling text input like that. Whatever is inside the text input must be represented as the plain text string.

Solving this problem with React seems trivial, because when you pass the state to the value attribute of the input - it's dead locked, that state is now single source of truth for this input and you're free to apply additional formatting to the value when it changes. It's still possible to set incorrect state but it would be coming from your React code, not user input.

I failed to follow similar approach in Vue. If instead of using v-model you go for similar binding of value and declaring @change event - it's not going to be as strict in React, in fact you can avoid handling value change part - it will remain out of sync just fine. Compared to React, @change also executes only after you're done with the input (blur/go out of focus), which is actually how it's supposed to work, something I didn't know about before.

You can use @input instead of @change to get similar behaviour, but sync problems remain.

Binding it to the computed value won't help either, you can still freely type in the input.

This made me wonder if the only way to do it would be by overriding events like onkeydown and preventing them if incorrect characters are entered, but you'll have to make sure to catch all other events like onpaste, this seems worse to maintain compared to React approach.

The last idea involved changing value attribute of the inputs directly via $ref, I can't even remember exactly what I did with these, but some of the solutions found on the internet follow this approach and it may be sufficient enough, but it's still a hacky solution.

Here's what I found by searching for Vue controlled input:

While two latter results are still on top of Google search, it takes effort or some desperation to notice the watch approach in making the input controllable. This is why I decided to write this note, to lampshade this simple way of doing it.

I tried to search for something other than controlled input afterwards:

Another bonus example

The example at the top is simplified and is only supposed to demonstrate the approach. How about something more real, like adding spaces to the number to maintain readability?

Here, $ref is still required to extract the input's caret position and calculate a new one, to ensure that you will have good experience without caret jumping around.

Please don't mind the code for adding spaces, I'm sure there's a better way for that.

By the way, due to the nature of watch, even if you change the data by other means than input - it still will be formatted properly, and I like that.