v-model on components
v-model works not only with input fields but also with your own components. It is handy when a component edits a single value of its parent: a search string, the chosen filter, the text of a new task. For this there is the defineModel macro, available since Vue 3.4.
A component with defineModel
Create src/components/TodoInput.vue:
<!-- src/components/TodoInput.vue -->
<script setup>
const text = defineModel()
</script>
<template>
<input v-model="text" placeholder="What needs doing?" class="todo-input">
</template>
<style scoped>
.todo-input {
padding: 8px;
width: 100%;
}
</style>defineModel() returns a ref linked to the parent's value. Reading it gives the parent's value, and writing it sends the new value to the parent.
Using it
<!-- src/App.vue -->
<script setup>
import { ref } from 'vue'
import TodoInput from './components/TodoInput.vue'
const newTodo = ref('')
</script>
<template>
<TodoInput v-model="newTodo" />
<p>New task: {{ newTodo }}</p>
</template>The text under the field repeats the input — the value from the component reaches the parent's newTodo. If the parent writes an empty string to newTodo, the field in TodoInput is cleared.
What happens inside
v-model on a component is a shorthand for a prop and an event:
<TodoInput
:modelValue="newTodo"
@update:modelValue="(value) => (newTodo = value)"
/>defineModel declares both the modelValue prop and the update:modelValue event, so you do not write defineProps and defineEmits by hand. Components in older code written before Vue 3.4 are built exactly this way.
Several models
A model can be named, and one component can have several v-model bindings. Create src/components/TodoFilter.vue:
<!-- src/components/TodoFilter.vue -->
<script setup>
const filter = defineModel('filter', { default: 'all' })
const query = defineModel('query', { default: '' })
</script>
<template>
<input v-model="query" placeholder="Search">
<button @click="filter = 'all'">All</button>
<button @click="filter = 'active'">Active</button>
<button @click="filter = 'done'">Done</button>
</template><!-- src/App.vue, template fragment -->
<TodoFilter v-model:filter="filter" v-model:query="query" />
<p>Filter: {{ filter }}, search: {{ query }}</p>The buttons change filter in the parent, and the field changes query. The filter and query variables are declared in App.vue with ref.
Common mistake
Receiving the value through defineProps and binding v-model to it inside the component:
<script setup>
const props = defineProps(['modelValue'])
</script>
<template>
<input v-model="props.modelValue">
</template>The component tries to change a prop that belongs to the parent; Vue warns, and the parent never receives the change. Use defineModel — it sends the change to the parent through an event.