v-model on a Custom Component

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Our new <form-input/> mostly works and we'll be able to reuse it to render all of our fields. The only missing piece is that when we type into the field, it doesn't update the data in CheckoutForm.

Normally we would fix this by emitting an event from the child component so that we can listen to that event in the parent component and update the data that lives there. So let's do that! And... then do something cooler.

Emitting and Listening to the Event

In form-input.vue, on the <input/> element, add @input="" and I'll use the inline format to emit an event. Let's call it input to match the name of the event that's used for a normal form element. For the data, let's just pass the new value $event.target.value.

<template>
<div class="form-group">
... lines 3 - 8
<input
... lines 10 - 17
@input="$emit('input', $event.target.value)"
>
... lines 20 - 25
</div>
</template>
... lines 28 - 57

Cool! Now, in the parent component, we can listen to this: @input="" and then immediately update the data with form.customerName = $event.

<template>
<div class="form-group">
... lines 3 - 8
<input
... lines 10 - 17
@input="$emit('input', $event.target.value)"
>
... lines 20 - 25
</div>
</template>
... lines 28 - 57

Alrighty! Let's try it! Move over, click to check out, type in a name... and look at the data. Perfect! It is updating.

How v-model Works

But... I do wish we could do something cooler. If this were a normal form element - and not a custom component - then we could simply use v-model instead of needing to pass :value="" and listen with @input="". That's because, for a normal form element, when you say v-model="customerName", it's equivalent to this: :value="customerName" and @input="" a function that sets customerName to $event.target.value.

It turns out that you can also use v-model on a custom component, but it means something slightly different. Using v-model on a custom component still causes a value prop to be passed... but the @input function isn't quite the same. Specifically, it expects the custom component to emit the input event and pass the new data as the event value.

So... wait a second. That exactly matches our setup! We have :value = the piece of data... and in @input="", the event data is the new value! So yes! We can remove these two lines and replace them with v-model!

Get rid of @input and :value and, instead, say v-model="form.customerName".

<template>
<div class="row p-3">
<div class="col-12">
<form>
<form-input
id="customerName"
v-model="form.customerName"
... lines 8 - 9
/>
</form>
</div>
</div>
</template>
... lines 15 - 39

Let's try it! Move over, hit check out, find CheckoutForm in the Vue dev tools and open up the data. Type in the box. How cool is that?

v-model and Vue 3

But one quick note! In Vue 3, using v-model on a custom component changes in two ways - it's actually one of the few breaking changes between Vue 2 and 3. First, v-model will now cause a prop called modelValue to be passed instead of value. So, in the form-input component, we would need to change the prop to modelValue and then use modelValue up in the template. Second, the name of the event changed. Your custom component will now need to emit an event called update: and then the name of that prop. So update:modelValue instead of input.

That's it! The name of the prop and event change, but everything else works exactly the same. Since our project is on Vue 2, I'll undo these changes.

Next: let's use our custom input to render all six form fields we need. We'll also use a few tricks to make this easier and more flexible.

Leave a comment!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
        "@symfony/webpack-encore": "^0.30.0", // 0.30.2
        "axios": "^0.19.2", // 0.19.2
        "bootstrap": "^4.4.1", // 4.5.3
        "core-js": "^3.0.0", // 3.6.5
        "eslint": "^6.7.2", // 6.8.0
        "eslint-config-airbnb-base": "^14.0.0", // 14.2.0
        "eslint-plugin-import": "^2.19.1", // 2.22.1
        "eslint-plugin-vue": "^6.0.1", // 6.2.2
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "sass": "^1.29.0", // 1.29.0
        "sass-loader": "^8.0.0", // 8.0.2
        "vue": "^2.6.11", // 2.6.12
        "vue-loader": "^15.9.1", // 15.9.4
        "vue-template-compiler": "^2.6.11", // 2.6.12
        "webpack-notifier": "^1.6.0" // 1.8.0
    }
}