Using v-model with custom components

2022 . Feb . 27 / Web DevelopmentProgramming

The Docs way

The documentation is clear, we have to do it this way to use v-model with a custom component:

template : Child.vue
<input v-bind:value="value" v-on:input="$emit('input', $event.target.value)" />
script : Child.vue
export default {
    props: ['value']
};

And the parent:

template : Parent.vue
<div>
    <Child v-model="value"></Child>
    {{ value }}
</div>
script : Parent.vue
import Child from './Child.vue';

export default {
    components: {
        Child
    },

    data() {
        return {
            value: ''
        };
    }
};

But what if we are creating another component that is going to be reused elsewhere? We'll have a Grandparent component that contains the Parent one, so now it will look like:

script : Parent.vue
import Child from './Child.vue';

export default {
    components: {
        Child
    },

    // Props instead of data()
    props: ['value']
};

And what is happening right now? We are getting the Vue warn:

But why? The value variable inside Parent.vue is not part of the data object but instead a prop. So, we can say it is owned now by a Grandparent component (using v-model).

Now, let's recall that v-model="value" it's the same as v-bind:value="value" v-on:input="value = $event" so, when the Child.vue component emits the input event, Parent.vue executes v-on:input="value = $event" changing the value inside a child component in regard to the Grandparent one, hence the Vue warn.

Computed Property as solution

The Vue warn tells us to use a data or computed property based on the prop's value to fix this, but how?

tl;dr:

template : Parent.vue
<div>
    <Child v-model="parentValue"></Child>
    {{ parentValue }}
</div>
script : Parent.vue
import Child from './Child.vue';

export default {
    components: {
        Child
    },

    props: ['value'],

    computed: {
        parentValue: {
            get() {
                return this.value;
            },

            set(setterValue) {
                this.$emit('input', setterValue);
            }
        }
    }
};

What is happening now is that when the Child.vue component emits the input event, Parent.vue changes the value of the parentValue variable, but because it is a computed value, it triggers another input event, and now the Grandparent component is the one that handles the real change. We have to notice that the getter of the computed value returns the value prop so we'll always have the updated value from the parent component (Grandparent in this case).

A full example

Now, that we know the behaviour of computed properties with v-model we can create components over other components over another component ad infinitum.

This example has a Child, Parent and Grandparent components within a Great-grandparent as the page where they reside. App.vue has the reactive property and the other components use only props and computed properties. We can change the value on either of the components without getting the Vue warn.

The Vue CLI was used to create the main project.

Child:

template : Child.vue
<div class="component-css">
    Child:
    <input v-model="childValue" />
    {{ childValue }}
</div>
script : Child.vue
export default {
    props: ['value'],

    computed: {
        childValue: {
            get() {
                return this.value;
            },

            set(setterValue) {
                this.$emit('input', setterValue);
            }
        }
    }
};

Parent:

template : Parent.vue
<div class="component-css">
    Parent:
    <input v-model="parentValue" />
    {{ parentValue }}
    <Child v-model="parentValue"></Child>
</div>
script : Parent.vue
import Child from './Child.vue';

export default {
    components: {
        Child
    },

    props: ['value'],

    computed: {
        parentValue: {
            get() {
                return this.value;
            },

            set(setterValue) {
                this.$emit('input', setterValue);
            }
        }
    }
};

Grandparent:

template : Grandparent.vue
<div class="component-css">
    Grandparent:
    <input v-model="grandparentValue" />
    {{ grandparentValue }}
    <Parent v-model="grandparentValue"></Parent>
</div>
script : Grandparent.vue
import Parent from './Parent.vue';

export default {
    components: {
        Parent
    },

    props: ['value'],

    computed: {
        grandparentValue: {
            get() {
                return this.value;
            },

            set(setterValue) {
                this.$emit('input', setterValue);
            }
        }
    }
};

App:

template : App.vue
<div id="app" class="component-css">
    App:
    <input v-model="someString" />
    {{ someString }}
    <Grandparent v-model="someString"></Grandparent>
</div>
script : App.vue
import Grandparent from './components/grandparent.vue';

export default {
    name: 'App',

    components: {
        Grandparent
    },

    data() {
        return {
            someString: ''
        };
    }
};
style : App.vue
.component-css {
    margin: 6px;
    padding: 6px;
    border: 1px dashed rgb(131, 131, 131);
}
Comments
If you have any doubt or question, or know how to improve this post, please feel free to write a comment.