Vue.js v-model binding on custom components
When working with form elements, Vue’s v-model directive comes useful for 2-way data binding.
But when you create your own custom form components and wrap input elements inside, it becomes hard to maintain the same 2-way binding with custom form component and parent component.
Problem
Imagine we have InputElement.vue component with input form element in it.
<template>
<div>
<input type="text"
placeholder="Your name" />
<span>Please enter your name</span>
</div>
</template>
And it is used in some other component like this:
<template>
<div>
<label>Enter your name</label>
<input-element />
</div>
</template>
<script>
import InputElement from '~/components/InputElement'
export default {
components: {
InputElement
},
data() {
return {
name: ''
}
}
}
</script>
Imagine we want to bind name data property to the input element,
but the actual input element is wrapped inside <input-element> component, not directly accessible to the parent component.
Luckily, Vue.js allows us to use v-model on custom components to establish 2-way input binding. But for this, first, we have to look closely at how v-model actually works.
How v-model works
In short, this directive basically does 2 things when applied to form element:
- Binds reactive property to the element’s
valueattribute - Handles
oninputevents fired from element to get new value
This means we can replace any input v-model directive with :value property binding and @input event handler:
// this is
<input v-model="someProperty" />
// same as this one
<input :value="someProperty"
@input="someProperty = $event.target.value"
/>
Of course, when working with native HTML form elements like input, select or textarea, it is always better to use v-model instead of manual binding. Because v-model is shorter, simpler to implement and
also, Vue.js is smart enough to bind values and events based on the type of form element. For example, v-model automatically detects that when used on <select> element it should look for onchange event, instead of oninput.
Now knowing how v-model works behind the scenes, we can use the same technique to add v-model directive to the custom component, then inside the component, we can get value as property and bind it to the input value.
Here’s our updated InputElement.vue component
<template>
<div>
<input type="text"
class="some-class"
placeholder="Input something"
:value="value" />
<span>Helper text here</span>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
Updated parent component that uses InputElement.vue:
<div>
<label>Input your name</label>
<input-element v-model="name" />
</div>
<script>
import InputElement from '~/components/InputElement'
export default {
components: {
InputElement
},
data() {
return {
name: ''
}
}
}
</script>
To notify v-model about value changes inside our component we need to fire input event every time value changes.
<template>
<div>
<input type="text"
class="some-class"
placeholder="Input something"
:value="value"
@input="$emit('input', $event.target.value)" />
<span>Helper text here</span>
</div>
</template>
<script>
export default {
props: ['value']
}
</script>
Here @input handler looks for oninput event fired from native <input> form element, then we fire our own input event to parent component with new input value as payload.
Now that we satisfied v-model directive with our value and event binding, we establish for 2-way binding with custom component.
Customizing default value and event names
Keep in mind that when attaching v-model to custom components,
by default, it always sends value property and expects input event.
But Vue.js allows us to change default names per component.
Simply add model configuration object to your component. This object can contain 2 properties:
propproperty defines howv-modelwill pass data to this component, default isvalueeventproperty defines which event name will be used for accepting changes forv-model, default isinput
Here’s our InputElement.vue component with custom model value and event name:
<template>
<div>
<input type="text"
class="some-class"
placeholder="Input something"
:value="typed"
@input="$emit('changed', $event)" />
<span>Helper text here</span>
</div>
</template>
<script>
export default {
model: {
prop: 'typed',
event: 'changed'
},
props: ['typed']
}
</script>
In this example when v-model added to <input-element> component, it will automatically send values as typed property and will expect updated values from changed event.
Usage tips
Learning this small trick with v-model directive opens many possibilities for establishing 2-way data binding with custom components.
Don’t be limited to just form elements. v-model does not care how you handle passed value property or when you fire input event back with the updated value.
You can create custom components that don’t even have any form elements but based on the component’s behavior, it can handle property and event on its own.