Javascript is required
·
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.
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

Lazy Load Vue Component When It Becomes Visible Image

Lazy Load Vue Component When It Becomes Visible

A Comprehensive Guide to Data Fetching in Nuxt 3 Image

A Comprehensive Guide to Data Fetching in Nuxt 3