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
value
attribute - Handles
oninput
events 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:
prop
property defines howv-model
will pass data to this component, default isvalue
event
property 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.