·
3 min read

Simpler Two-Way Binding in Vue With defineModel

Simpler Two-Way Binding in Vue With defineModel Image

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.

Info

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:

Child.vue
1<script setup lang="ts">
2  const props = defineProps(['modelValue'])
3  const emit = defineEmits(['update:modelValue'])
4</script>
5
6<template>
7  <input
8    :value="modelValue"
9    @input="emit('update:modelValue', $event.target.value)"
10  />
11</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:

Child.vue
1<script setup lang="ts">
2const model = defineModel()
3</script>
4
5<template>
6  <input v-model="model">
7</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:

Child.vue
1<script setup lang="ts">
2// making the v-model required
3const model = defineModel({ required: true })
4
5// providing a default value
6const model = defineModel({ default: 0 })
7</script>

Multiple v-model bindings

If you have multiple v-model bindings in your component, you can use defineModels to define multiple models at once:

Parent.vue
1<template>
2  <Child
3    v-model:first-name="firstName"
4    v-model:last-name="lastName"
5  />
6</template>
Child.vue
1<script setup lang="ts">
2const firstName = defineModel('firstName')
3const lastName = defineModel('lastName')
4</script>
5
6<template>
7  <input v-model="firstName" />
8  <input v-model="lastName" />
9</template>

If prop options are also needed, you can pass them after the model name:

Child.vue
1<script setup lang="ts">
2const firstName = defineModel('firstName', { required: true })
3const lastName = defineModel('lastName', { default: '-' })
4</script>
5
6<template>
7  <input v-model="firstName" />
8  <input v-model="lastName" />
9</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:

Parent.vue
1<template>
2  <Child v-model.uppercase="message" />
3</template>
Child.vue
1<script setup lang="ts">
2const [model, modifiers] = defineModel({
3  set(value) {
4    if (modifiers.capitalize) {
5      return value.toUpperCase()
6    }
7    return value
8  }
9})
10</script>
11
12<template>
13  <input v-model="model" />
14</template>

Typing

You can define the type of the model value inside the options object:

Child.vue
1<script setup lang="ts">
2const count = defineModel({
3  type: Number,
4  default: 0
5})
6</script>

If you are using TypeScript, you can also define the type of the model value and modifiers in the following way:

Child.vue
1<script setup lang="ts">
2const modelValue = defineModel<string>()
3//    ^? Ref<string | undefined>
4
5// default model with options, required removes possible undefined values
6const modelValue = defineModel<string>({ required: true })
7//    ^? Ref<string>
8
9const [modelValue, modifiers] = defineModel<string, 'trim' | 'uppercase'>()
10//                 ^? Record<'trim' | 'uppercase', true | undefined>
11</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:

I will never share any of your personal data. You can unsubscribe at any time.

If you found this article helpful.You will love these ones as well.
Analyze Memory Leaks in Your Nuxt App Image

Analyze Memory Leaks in Your Nuxt App

Self-Host Your Nuxt App With Coolify Image

Self-Host Your Nuxt App With Coolify

Unlocking the Power of v-for Loops in Vue With These Useful Tips Image

Unlocking the Power of v-for Loops in Vue With These Useful Tips

Focus & Code Diff in Nuxt Content Code Blocks Image

Focus & Code Diff in Nuxt Content Code Blocks