Vue 3: Making An Input Component with v-model Support

Vue.js is a powerful JavaScript library that makes creating complex web apps more straightforward than ever. Out of the box, Vue.js gives you most of the tools you need to get going. However, something that seems simple can cause some tricky behavior. One common example I’ve seen is having a component with an input interact with the parent via v-model. Generally, when you create an input component, you need the data it interacts with to update on the parent component.

There are many ways to approach updating the parent’s data, but the cleanest way in my opinion is to use the component in the same way you would with a normal input with v-model. Let dive into this method.

How v-model Works

Normally, you’d define a v-model value to bind a piece of data to an input like so:

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

It works wonderfully and there’s not much more to it. Vue actually is doing a couple of things for you behind the scenes here. Once you make a child component with an input that needs a binding to a piece of data on the parent, you’ll see that this no longer works.

<template>
  <section id="parent">
    <ChildComponent v-model="username" />
  </section>
</template>
<script>
  import ChildComponent from '@/Components/ChildComponent'
  export default {
    components: {
      ChildComponent
    },
    data() {
      return {
        username: 'user'
      }
    }
  }
</script>
// ChildComponent
<template>
  <input placeholder="Update Username" />
</template>

The expectation is that the input will update normally with v-model defined on the component in the parent. This is not the case. Here’s what v-model needs to actually work:

A value on the bound input element – Behind the scenes, Vue adds a :value on the input in order to keep the input updated with the bound data item.

When the input is updated, a custom event is emitted to update the bound data prop – In the opposite direction, Vue uses this custom event to update the data prop when the input is changed.

That’s where two-way data binding comes from: the input communicates with the data prop and the input is updated with the data prop via :value. Let’s put this in action in the new child component.

Applying This To The Child Component

<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
  props: ['modelValue'],
  emits: ['update:modelValue'],
</script>

This should update the bound property on the parent component when used:

<ChildComponent v-model="username" />

You likely notice two new properties here: modelValue and update:modelValue. These are used by Vue internally, so the naming here is important. These aren’t properties you can name whatever you want. What are they exactly?

modelValue is used by Vue to keep the input updated with the bound data prop. You’ll notice it’s passed in as a prop and used as the :value. modelValue is what’s passed in when you use v-model on the parent component. In this case, it’s the value of username.

Update :modelValue is also used by Vue internally for updating the bound data prop when the input is changed. You’ll notice that an event is emitted on @input. All we’re doing here is emitting the very specific update:modelValue and passing the $event.target.value to it. This is how Vue updates the bound data prop when you type in an input.

Don’t forget the emits: ['update:modelValue'] as this will avoid those annoying console errors!

That’s pretty much it! Now whenever you make a component you’ll know how to replicate the v-model logic that Vue uses behind the scenes. More importantly, you unlock greater power when making components.

Comments