Vue.js course Β· Module 7: Props & Events

v-model on Components

5 min read
In this lesson5

The station's transmission form has a "Message content" field. The parent keeps the content in a ref, while the field itself lives in a MarsInput component. Props will pass the value down and an event will send the change back up - but writing that pair for every form field quickly gets tedious. Fortunately, v-model also works on custom components - it's like two-way data synchronization between Earth and Mars!

On native HTML elements v-model is a shorthand that depends on the element: a text <input> and a <textarea> combine value with the input event, checkboxes and radio buttons combine checked with the change event, and a <select> combines value with the change event. On a component there is a single rule, and you are the one who fulfils it.

How does v-model work on a component?

The Vue compiler expands v-model on a component into a modelValue prop and a listener for the update:modelValue event. Both notations below do exactly the same thing:

1<!-- Parent uses v-model -->
2<MarsInput v-model="message" />
3
4<!-- Is equivalent to: -->
5<MarsInput
6  :modelValue="message"
7  @update:modelValue="message = $event"
8/>

In the second version $event is the first argument sent by the child, that is the field's new content. The parent assigns it to message, so the parent remains the owner of the data.

Implementation in the component

The component must:

  1. Accept a modelValue prop
  2. Emit an update:modelValue event

In practice both points fit into a few lines:

1<!-- MarsInput.vue -->
2<script setup>
3defineProps(['modelValue'])
4const emit = defineEmits(['update:modelValue'])
5</script>
6
7<template>
8  <input
9    :value="modelValue"
10    @input="emit('update:modelValue', $event.target.value)"
11  />
12</template>

The parent has not changed by a single character - it still writes <MarsInput v-model="message" />. The child does all the work: it displays modelValue and, with every typed character, sends an event with the new content. It never overwrites the prop itself, in line with the one-way data flow rule - it only asks the parent for a change.

defineModel - a shorter syntax (Vue 3.4+)

Since Vue 3.4 you can write the same component more briefly with the defineModel macro. It returns a ref: reading it gives the value of the modelValue prop, and writing to it emits update:modelValue. That is why a plain v-model on the <input> field is enough inside:

1<!-- MarsInput.vue -->
2<script setup>
3const model = defineModel()
4</script>
5
6<template>
7  <input v-model="model" />
8</template>

Under the hood the same mechanism is at work - a prop and an event - so the parent sees no difference at all. The Vue documentation recommends defineModel for new code, and the manual version is still worth knowing, because you will meet it in older projects and libraries.

Multiple v-model

Vue 3 allows multiple v-model bindings on one component. The argument after the colon changes the name of the pair: v-model:first-name connects to the firstName prop and the update:firstName event:

1<!-- Parent -->
2<template>
3  <CrewForm
4    v-model:first-name="firstName"
5    v-model:last-name="lastName"
6  />
7</template>
8
9<!-- CrewForm.vue -->
10<script setup>
11defineProps(['firstName', 'lastName'])
12const emit = defineEmits(['update:firstName', 'update:lastName'])
13</script>
14
15<template>
16  <input
17    :value="firstName"
18    @input="emit('update:firstName', $event.target.value)"
19  />
20  <input
21    :value="lastName"
22    @input="emit('update:lastName', $event.target.value)"
23  />
24</template>

In the parent's template you can write the argument in kebab-case or in camelCase, as v-model:firstName - both forms work in .vue files. With defineModel you only need to pass the name: const firstName = defineModel('firstName').

v-model Modifiers

You can create your own modifiers. When used on v-model, they reach the child in the modelModifiers prop as an object, for example { uppercase: true }:

1<!-- Parent -->
2<MarsInput v-model.uppercase="text" />
3
4<!-- MarsInput.vue -->
5<script setup>
6const props = defineProps({
7  modelValue: String,
8  modelModifiers: { default: () => ({}) }
9})
10const emit = defineEmits(['update:modelValue'])
11
12function onInput(e) {
13  let value = e.target.value
14  if (props.modelModifiers.uppercase) {
15    value = value.toUpperCase()
16  }
17  emit('update:modelValue', value)
18}
19</script>
20
21<template>
22  <input :value="modelValue" @input="onInput" />
23</template>

The component checks the flag and transforms the text before sending it, so the parent already receives capital letters. For a named v-model:first-name, the modifiers prop is called firstNameModifiers. With defineModel you read the modifiers as the second element of the returned pair, and you put the transformation in the set option:

1<script setup>
2const [model, modifiers] = defineModel({
3  set(value) {
4    return modifiers.uppercase ? value.toUpperCase() : value
5  }
6})
7</script>

The set function receives the value being written to the model and returns what should reach the parent. The parent changes nothing - it still writes <MarsInput v-model.uppercase="text" />.

My advice: in new components reach for defineModel, because it means less code and fewer places for a typo in an event name. In the editor below, MarsInput and CrewForm use the manual version - try rewriting them with defineModel. Just remember that v-model connects two neighbouring levels; in the next lesson you will send data five floors down without passing it through every floor.

Remember: v-model on a component is a prop and an event paired into one duplex channel.

Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3import MarsInput from './MarsInput.vue'
4import CrewForm from './CrewForm.vue'
5
6const message = ref('Hello from Mars!')
7const firstName = ref('Nova')
8const lastName = ref('Commander')
9</script>
10
11<template>
12  <div class="nova-lab">
13    <h1>v-model on Components</h1>
14
15    <h2>Single v-model:</h2>
16    <MarsInput v-model="message" />
17    <p>Parent value: {{ message }}</p>
18
19    <h2>Multiple v-model:</h2>
20    <CrewForm
21      v-model:first-name="firstName"
22      v-model:last-name="lastName"
23    />
24    <p>Name: {{ firstName }} {{ lastName }}</p>
25  </div>
26</template>
27
28<style scoped>
29.nova-lab {
30  background: #0a0e27;
31  color: #00ff88;
32  min-height: 100vh;
33  padding: 20px;
34  font-family: monospace;
35}
36h1 { color: #00ff88; margin-bottom: 20px; }
37h2 { color: #00b4d8; margin: 15px 0 10px; }
38p { margin: 8px 0; }
39</style>

Spotted a mistake in this lesson?

Check yourself

Answer the questions from this lesson. Pick an answer to see right away whether it is correct.

  1. 1. What is v-model on a custom component?

  2. 2. What must a component do to support v-model?

These are 2 of 3 questions for this lesson. Solve the rest in the game.

Hands-on tasks in the game

  • Code editor

    Create a MarsInput with full v-model support (modelValue + update:modelValue).

  • Vertical ordering

    Arrange the steps for implementing v-model in a component:

  • Code editor

    Create a component with two v-model bindings: v-model:firstName and v-model:lastName.

Useful articles