The web is made up of forms! It's hard to find a public-facing website without a way of taking some kind of input from the visitor. And providing an effective yet simple way to build a form is an essential aspect of a good frontend framework. Enters Vue 🎉
Vue provides a pleasant two-way data binding API, in the form of the v-model
derivative. v-model
creates a reactive connection between an HTML element and JavaScript, making it fun to work with inputs of all sorts! Today we'll look into a few ways of building reusable Vue components with HTML input elements. Let's get started!
Creating a reactive input field
Input fields are pretty straightforward in Vue, visibly not so different from HTML.
<template>
<label for="username">
Username
<input type="text" name="username" id="username" />
</label>
</template>
Here, we're taking the username
as a text
type input. It doesn't do anything with the input yet, we'll add v-model
to handle that.
<template>
<div>
<label for="username">
Username
<input type="text" name="username" id="username" v-model="username" />
</label>
<p>{{ username }}</p>
</div>
</template>
<script>
export default {
data() {
return {
username: "",
};
},
};
</script>
Firstly, we're adding v-model
for the input
element, which effectively binds the data username
with the input value. It's a two-way bind, meaning the value inside input
will change if we change the username
inside data()
and vice-versa. I've added the username
inside <p>
just below the input to show this change live.
It seems to be working nicely! However, updating the input
value in real-time is not a common scenario for forms, the value should only get updated when the user completes writing or focuses somewhere else on the page. What v-model
does is that it updates the data in every oninput
event (Not for languages that use IME, more here), whereas we want it to update on every onchange
event. v-model
has a built-in modifier that can achieve that, .lazy
. Let's try that.
<input type="text" name="username" id="username" v-model.lazy="username" />
The username only updates when we're done writing and press enter or focus outside the input box.
Great! All Good for now. In the next step, we'll try to consolidate this input-related logic into a component.
Creating reactive components
It's a common practice to build Vue components based on standard HTML form fields to use across the project. While building such components, sometimes we have to make the input value available from outside the component or from the parent component. The most common way of doing so without using a state management library is to use the $emit
instance method. It's available as,
$emit
inside<template></template>
this.$emit
inside<script></script>
When called, the $emit(eventName, args)
method triggers an event and passes arguments to that event. Then, the listeners that listen to that specific event, process the arguments and take necessary actions. To implement this pattern of using $emit
, first, let's create the BaseTextInput
component,
Create a file named
BaseTextInput.vue
(or something else)Move the input field-related code there. It'll primarily look like this,
<template>
<div>
<label for="username">
Username
<input
type="text"
name="username"
id="username"
v-model.lazy="username"
/>
</label>
</div>
</template>
<script>
export default {
data() {
return {
username: "",
};
},
};
</script>
- Now let's add a custom event that'll emit from our component when the input changes. In Vue, the
@change
derivative is used as theonchange
event handler.
<input
type="text"
name="username"
id="username"
v-model.lazy="username"
@change="$emit('input-changed', username)"
/>
This emits an event called input-changed
and passes the username
as the argument of the listeners' callback function.
Note: use
kebab-case
when naming events, as suggested by Vue documentation here.
- Now let's add a listener in the parent/enclosing component where the input component is used. The listener will trigger a callback function that we'll define in the
methods
attribute inside<script>
. The parent component will look something like this,
<template>
<BaseTextInput @input-changed="handleInput" />
<p>{{ username }}</p>
</template>
<script>
import BaseTextInput from "./components/BaseTextInput";
export default {
name: "App",
data() {
return {
username: "",
};
},
methods: {
handleInput(value) {
this.username = value;
},
},
components: {
BaseTextInput,
},
};
</script>
Result:
Looks like it's working like before. Great! Let's see how we can improve on this.
Creating better reactive components
The pattern we've followed till now is quite valid, but there's room for improvement. To do so, we have to dive deeper into the inner workings of the v-model
directive. When we use it on input
elements like this,
<input type="text" v-model="username" />
What Vue does under the hood is this,
<input
type="text"
v-bind:value="username"
v-on:input="(e) => (username = e.target.value)"
/>
It gets the username
data using one-way v-bind
directive, and updates the username on input using an inline arrow function. So technically, we could've wrote our previous BaseTextInput
components input
element like this,
<input
type="text"
name="username"
id="username"
:value="username"
@change="handleInput"
/>
The handleInput
function is defined inside methods
in a simple manner.
handleInput(e) {
this.username = e.target.value;
this.$emit("input-changed", this.username);
}
This approach makes us write a couple more lines of code than the previous implementation, but gives us finer control over the data flow both inside and outside the component. But have we reached the better part yet? Not quite.
We are maintaining two local states inside both the parent and child components.
We're resorting to custom events when we could've used Vue's built-in events.
There's no way to dictate the value of
input
from the parent.
Let's solve these issues by dissecting the v-model
in our child component!
- Remove the local state variable from
data()
and update the value of theinput
element to be dependent on aprop
from the parent.
<template>
<div>
<label for="username">
Username
<input
type="text"
name="username"
id="username"
:value="username"
@change="$emit('update:username', $event.target.value)"
/>
</label>
</div>
</template>
<script>
export default {
props: {
username: String,
},
emits: ["update:username"],
};
</script>
We're getting a prop
named username
from the parent, and binding it with input
s value. This means whenever we pass a new prop from the parent, the input element will re-render and set the value of username
as its value. To do the opposite, that is to reflect changes from input
to parent, we're emitting an event on change (@change
) and passing the input value as an argument. But what's with the event named update:username
? It's a built-in event with a custom argument. Let's discuss this in the next step.
- Update the parent component to use
v-model
instead of listening to our custom event. Previously, we mentioned that under the hood Vue uses:value
and@input
directives to achievev-model
s functionalities. When used with components,v-model
s implementation is a bit different, where this,
<BaseTextInput v-model="username" />
means,
<BaseTextInput
:modelValue="username"
@update:modelValue="newValue => username = newValue"
/>
Please note, For Vue 2, the implementation is a bit different. Instead of
@update:modelValue
, it usesinput
event. Further details can be found at the Vue Documentation.
By default, it listens to this @update:modelValue
event. We can change the modelValue
variable to any other name we want if we use the explicit version instead of directly using v-model
. So the parent component can be like this.
<template>
<BaseTextInput v-model:username="username" />
<p>{{ username }}</p>
</template>
<script>
import BaseTextInput from "./components/BaseTextInput";
export default {
name: "App",
data() {
return {
username: "",
};
},
components: {
BaseTextInput,
},
};
</script>
Check here we've passed an additional argument, username
, with v-model
. This changes the event signature from @update:modelValue
to @update:username
inside the child component. Pretty neat, right?
The Final result is available at this code sandbox.
Summary
The post already got quite big, so let's stop here today and recap what is what in v-model
,
v-model
is a two-way data-binding directive provided by Vue.It reactively binds an element from
template
with the value fromscript
.Under the hood, it uses
v-bind:value
andv-on:input
directives to achieve this reactivity.We can pass modifiers like
.lazy
which updatesv-on:input
tov-on:change
forv-model
.When used on a component level,
v-model
usesv-bind:modelValue
andv-on:update:modelValue
wheremodelValue
is the default.We can tap into these events and values using
$emit
from the child component and modify them to our needs.
Apart from these, there are some other topics related to models and components like using multiple v-model
s for a single component, custom model modifiers, and so on. Maybe we'll discuss those details, along with how this building pattern can be used with other types of inputs, such as number
, radio
, checkbox
, and select
some other day! Till then, Peace ☮️