
v-model
is a powerful feature in Vue that allows you to create two-way data bindings on your components. However, defining the props and emits in every component can be a bit verbose.
In this article, I'll show you how to simplify two-way binding in Vue with the defineModel
compiler-macro, which is now the recommended way to define v-model
bindings in Vue 3.4 and later.
defineModel()
is a new feature in Vue 3.4. Make sure you are using Vue 3.4 or later to use this feature.The "Problem"
When you create a component that uses v-model
, you need to define a prop and an emit for the value. For example, if you have a component that uses v-model
to bind to a value
prop, you would need to define the following:
<script setup lang="ts">
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
The Solution
defineModel
is a new <script setup>
macro that aims to simplify the implementation of components that support v-model
. It automatically defines the modelValue
prop and the update:modelValue
emit for you.
Let's rewrite the above example using defineModel
:
<script setup lang="ts">
const model = defineModel()
</script>
<template>
<input v-model="model">
</template>
I love this simple and clean syntax. It makes the code much easier to read and write.
defineModel()
returns a ref
, which is automatically bound to the modelValue
prop and emits the update:modelValue
event when the value changes. The .value
is synced with the value bound by the parent v-model
. When the ref
is updated, the value bound by the parent is automatically updated.
This allows us to use v-model
directly on the native input element without additional code.
Options
defineModel
also accepts an optional options object to configure the behavior of the model:
<script setup lang="ts">
// making the v-model required
const model = defineModel({ required: true })
// providing a default value
const model = defineModel({ default: 0 })
</script>
v-model
bindings Multiple
If you have multiple v-model
bindings in your component, you can use defineModels
to define multiple models at once:
<template>
<Child
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
</template>
<script setup lang="ts">
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input v-model="firstName" />
<input v-model="lastName" />
</template>
If prop options are also needed, you can pass them after the model name:
<script setup lang="ts">
const firstName = defineModel('firstName', { required: true })
const lastName = defineModel('lastName', { default: '-' })
</script>
<template>
<input v-model="firstName" />
<input v-model="lastName" />
</template>
Modifiers
defineModel
also supports modifiers. You can use modifiers to customize the behavior of the model. Let's take a look at a simple modifier that modifies every character of the model value and makes it uppercase:
<template>
<Child v-model.uppercase="message" />
</template>
<script setup lang="ts">
const [model, modifiers] = defineModel({
set(value) {
if (modifiers.capitalize) {
return value.toUpperCase()
}
return value
}
})
</script>
<template>
<input v-model="model" />
</template>
Typing
You can define the type of the model value inside the options object:
<script setup lang="ts">
const count = defineModel({
type: Number,
default: 0
})
</script>
If you are using TypeScript, you can also define the type of the model value and modifiers in the following way:
<script setup lang="ts">
const modelValue = defineModel<string>()
// ^? Ref<string | undefined>
// default model with options, required removes possible undefined values
const modelValue = defineModel<string>({ required: true })
// ^? Ref<string>
const [modelValue, modifiers] = defineModel<string, 'trim' | 'uppercase'>()
// ^? Record<'trim' | 'uppercase', true | undefined>
</script>
StackBlitz
Try it yourself in the following StackBlitz project:
Conclusion
I love the new defineModel
compiler macro. It makes two-way binding in Vue much simpler and cleaner. I hope you find this feature as helpful as I do.
If you liked this article, follow me on X to get notified about my new blog posts and more content.
Alternatively (or additionally), you can subscribe to my weekly Vue & Nuxt newsletter: