defineModel()
The defineModel() macro is used to create two-way data binding between parent and child components. It automatically creates a prop and an emit event, creating a clean and simple approach to passing data between parent and child components.
It is only available to use inside of <script setup>, and importing is not required. The defineModel() macro was introduced in Vue 3.4+ and provides a cleaner alternative to manually implementing two-way binding with defineProps() and defineEmits().
If not using
<script setup>, you would need to manually implement two-way binding usingpropsandemitsoptions withmodelValueandupdate:modelValue.
Syntax
// Basic syntax
const model = defineModel();
// With type definition
const model = defineModel({
type: Type,
required: Boolean,
default: DefaultValue
});
// With custom name (instead of modelValue)
const title = defineModel('title', {
type: String,
required: true
});
Parameters:
- First parameter (optional): A string representing the model name. If omitted, defaults to
'modelValue'. - Second parameter (optional): An object with the following properties:
- type: The constructor function (
String,Number,Boolean,Date,Array,Object, etc.). - required: If the model is required (
Boolean). - default: The default value or a function that returns the default value.
- type: The constructor function (
Return Value:
Returns a ref() that can be used directly with v-model in the template. Changes to this ref automatically sync with the parent component.
Basic usage
Here's a simple example of using defineModel() in an input component. This component creates two-way binding for a text input:
<script setup>
const model = defineModel({
type: String,
default: ''
});
</script>
<template>
<input v-model="model" type="text" placeholder="Enter text" />
</template>
The component is used in the parent with v-model:
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const textValue = ref('');
</script>
<template>
<CustomInput v-model="textValue" />
<p>Current value: {{ textValue }}</p>
</template>
Data is now automatically passed between parent and child components.
The
defineModel()macro automatically creates amodelValueprop and anupdate:modelValueemit event. When you usev-modelon the component, Vue handles the prop binding and event listening automatically.
Type validation
You can specify the type of data the model accepts:
// String model
const name = defineModel({
type: String,
default: ''
});
// Number model
const count = defineModel({
type: Number,
default: 0
});
// Boolean model
const isActive = defineModel({
type: Boolean,
default: false
});
// Array model
const items = defineModel({
type: Array,
default: () => []
});
// Object model
const user = defineModel({
type: Object,
default: () => ({})
});
Required models
You can mark a model as required to ensure it's always provided by the parent:
const title = defineModel({
type: String,
required: true
});
If the parent doesn't provide a value for a required model, Vue will show a warning in the console.
Default values
Models can have default values when not provided by the parent:
// Simple default value
const message = defineModel({
type: String,
default: 'Hello World'
});
// Function that returns default (for objects and arrays)
const formData = defineModel({
type: Object,
default: () => ({
name: '',
email: '',
age: 0
})
});
const tags = defineModel({
type: Array,
default: () => []
});
Custom model names
By default, defineModel() uses modelValue as the prop name. You can specify a custom name as the first parameter:
<script setup>
const title = defineModel('title', {
type: String,
required: true
});
const description = defineModel('description', {
type: String,
default: ''
});
</script>
<template>
<input v-model="title" type="text" placeholder="Title" />
<textarea v-model="description" placeholder="Description"></textarea>
</template>
In the parent component, use the custom name with v-model:
<script setup>
import { ref } from 'vue';
import CustomForm from './CustomForm.vue';
const formTitle = ref('');
const formDescription = ref('');
</script>
<template>
<CustomForm
v-model:title="formTitle"
v-model:description="formDescription"
/>
</template>
Multiple models
You can use multiple defineModel() calls to create multiple two-way bindings:
<script setup>
const firstName = defineModel('firstName', {
type: String,
default: ''
});
const lastName = defineModel('lastName', {
type: String,
default: ''
});
const email = defineModel('email', {
type: String,
default: ''
});
</script>
<template>
<div class="form">
<input v-model="firstName" type="text" placeholder="First Name" />
<input v-model="lastName" type="text" placeholder="Last Name" />
<input v-model="email" type="email" placeholder="Email" />
</div>
</template>
Parent usage:
<template>
<UserForm
v-model:first-name="user.firstName"
v-model:last-name="user.lastName"
v-model:email="user.email"
/>
</template>
Practical example: Form component
The following example shows how defineModel() can be used in a form component to create two-way binding for form data:
<script setup>
const formData = defineModel({
type: Object,
default: () => ({
name: '',
email: '',
message: ''
})
});
</script>
<template>
<form class="contact-form">
<div class="form-group">
<label for="name">Name</label>
<input
id="name"
v-model="formData.name"
type="text"
placeholder="Your name"
/>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
v-model="formData.email"
type="email"
placeholder="[email protected]"
/>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea
id="message"
v-model="formData.message"
placeholder="Your message"
rows="4"
></textarea>
</div>
</form>
</template>
The parent component uses v-model to bind the form data:
<script setup>
import { ref } from 'vue';
import ContactForm from './ContactForm.vue';
const contactFormData = ref({
name: '',
email: '',
message: ''
});
</script>
<template>
<ContactForm v-model="contactFormData" />
<div class="preview">
<h3>Preview:</h3>
<p>Name: {{ contactFormData.name }}</p>
<p>Email: {{ contactFormData.email }}</p>
<p>Message: {{ contactFormData.message }}</p>
</div>
</template>
Comparison: Traditional vs defineModel
Traditional approach (before Vue 3.4)
<script setup>
const props = defineProps({
modelValue: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:modelValue']);
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
type="text"
/>
</template>
Modern approach with defineModel
<script setup>
const model = defineModel({
type: String,
default: ''
});
</script>
<template>
<input v-model="model" type="text" />
</template>
The defineModel() approach is much simpler and requires less boilerplate code!
Using with TypeScript
When using TypeScript, you can define models with explicit types:
// String model
const title = defineModel<string>({
default: ''
});
// Number model
const count = defineModel<number>({
default: 0
});
// Object model with interface
interface User {
name: string;
email: string;
age: number;
}
const user = defineModel<User>({
default: () => ({
name: '',
email: '',
age: 0
})
});