Best way to add v-model to custom components

If you’re using vue.js and trying to create you own components, at some point in time you’ll need to somehow pass some form of data from a child component to the parent. There are many ways to accomplish the same result. Some use an external store, some use the window object and custom events and most developers use an event bus.

Vue.js already offers a custom directive for two way data-binding. To enable two way data binding for an element all we do is to use the v-model directive.

Under the hood, this is nothing but a simple event emitter that send the data from a child to its parent by using predefined events.

How does v-model work ?

<input type="text" v-model="message" />

As we talked about earlier, v-model is nothing but a glorified event emitter. It basically binds the value prop of the child to have parent-to-child data binding. so something like this :

<input type="text" v-bind:value="message" />

we can use :value instead of writing out the full v-bind directive.

to handle child-to-parent data binding all we need to do is to emit some kind of event that and store the info in the same property that we used for the value :

<input type="text" :value="message" @input=" ... " />

Each event can send some data with it. So to fill in the ‘ … ’ gap, we need a function that can retrieve that data and store it in our message property.

we can write a simple function in methods to do exactly that:

data: () => ({
message: "hello, medium!",
}),
methods: {
handelInput(val) {
this.message = val'
},
},

so now our original input would look something like this :

<input type="text" :value="message" @input="handelInput" />

to make this shorter and prettier to look at we can use ES6 arrow functions and re-write this whole thing like so:

<input type="text" :value="message" @input="val => message = val" />

to that is basically what v-model does. It binds value to a property and listens for input event and sets the value of bound property to whatever input spits at it.

TL;DR

so all we need to do is to add a value prop to our component and emit an input event whenever our value prop changes.

to do that all we need to do is something like so:

/* ./components/textInput.vue */<template>    <input type="text" v-model="msg" /></template><script>export default {
props: ["value"],
data: () => ({
msg: this.value,
}),
watch: {
msg(newval){
this.$emit("input", newval);
},
},
};
</script>

and in the parent components we can write something like this :

/* ./views/homeView.vue */<template>    <p> {{ message }} </p>
<TextInput v-model="message"></TextInput>
</template><script>
import TextInput from "../components/textInput.vue";
export default {
components:{
TextInput,
},
data: () => ({
message: "hello, medium!",
}),
};
</script>

Best Way To Add v-model To Custom Components

But this is not the best way to implement v-model for custom components. Because the structure of our component is built in such way that it could easily get de-synced from its parent component.

To avoid such issues we can use ``computed`` properties.

Something that not many vue.js developers know about or use that that computed properties can have different behaviors based on how you are treating them. In other words they have to different function for when you are getting the value from them or for when you are setting a value to them.

Using computed properties mean the child component wont have a state and it cannot get de-synced from its parent. The only way for the data to change in our child component is to change it from its parent.

computed: {
message: {
get() {
return this.value;
},
set(setvalue) {
this.$emit("input", setvalue);
},
},
},

In this code snippet the message returns value when you’re trying to get its value and will emit an input event with the new value when you try to change its value. This will cause its parent to update the state and change the value of the bound property.

So our entire component would look like this :

/* ./components/textInput.vue */<template><input type="text" v-model="msg" /></template><script>export default {
props: ["value"],
computed: {
msg: {
get() {
return this.value;
},
set(setvalue) {
this.$emit("input", setvalue);
},
},
},
};
</script>

And out parent component would look exactly the same as before :

/* ./views/homeView.vue */<template><p> {{ message }} </p>
<TextInput v-model="message"></TextInput>
</template><script>
import TextInput from "../components/textInput.vue";
export default {
components:{
TextInput,
},
data: () => ({
message: "hello, medium!",
}),
};
</script>

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store