DEV Community

Blind Kai
Blind Kai

Posted on

Using v-model with objects in Vue3

Motivation

When I was using Vue2 along with vue-class-component and vue-property-decorator it was easy to synchronize v-models between components simply using @ModelSync(). When Vue 3 came out with its Composition API another way was needed to achieve the same result as if Class Component was used.

Implementation

If you're already familiar with the capabilities of Composition API, then simply use computed within setup to update the modelValue whenever it changes.

1) In child component define a model property with the default value

import { defineComponent } from 'vue';

<script>
export default defineComponent({
  name: 'FancyComponent',
  props: {
    modelValue: {           // Declare the property
      type: Object,
      default: () => ({}),  // Do not forget about default value
    },
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

2) In setup() define a computed property and expose it:

<template>
  <div>
    <input v-model="theModel.foo" />  <!-- Usage of model -->
  </div>
</template>

<script>
import { defineComponent, computed } from 'vue';

export default defineComponent({
  name: 'FancyComponent',
  emits: ['update:modelValue'], // The component emits an event
  props: {
    modelValue: {
      type: Object,
      default: () => ({}),
    },
  },
  setup(props, { emit }) {
    const theModel = computed({  // Use computed to wrap the object
      get: () => props.modelValue,
      set: (value) => emit('update:modelValue', value),
    });

    return { theModel };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

3) In parent component use v-model directive:

<template>
  <FancyComponent v-model="someObject" />
</template>
Enter fullscreen mode Exit fullscreen mode

TypeScript

In the case of using TypeScript, there is one minor addition to the code above. PropType<T> is used it order to annotate the model type.

<script>
import { defineComponent, computed, PropType } from 'vue';

interface OurModelType {
  foo: string;
}

export default defineComponent({
  name: 'FancyComponent',
  emits: ['update:modelValue'],
  props: {
    modelValue: {
      type: Object as PropType<OurModelType>, // Type Annotation
      default: () => ({}),
    },
  },
  setup(props, { emit }) {
    const theModel = computed({
      get: () => props.modelValue,
      set: (value) => emit('update:modelValue', value),
    });

    return { theModel };
  }
});
</script>
Enter fullscreen mode Exit fullscreen mode

And it's all you need to know to pass reactive objects into your custom components as v-model.

Top comments (11)

Collapse
 
hansonfang profile image
hansonfang

In the 'get' of computed, is the reactivity deeply? I used this way back in vue2, turn out I had to use watch with deep option to make it work.

Collapse
 
blindkai profile image
Blind Kai • Edited

It depends. If you define new properties - you probably want to use Vue.set or $vm.set. Vue 2 works using patching mechanism that modify object fields to use getters and setters, and it's done during initialization. Any other field won't be reactive if you don't set it with options written above.

So it all depends on if you're creating completely new objects or adjust some properties you probably want to use Vue.set or Object.assign({}, oldObject, ...) etc.

You could find more descriptive information with examples in the official documentation.

Collapse
 
hansonfang profile image
hansonfang

That's make sense, thanks

Collapse
 
lucasctd profile image
Lucas Reis

You saved my day, I was struggling a lot with this XD Everytime I emitted the updated event I was getting an undefined object error, the default option saved me =D

In my case I am only using the emit event inside a watch (cuz I have a custom condition to update other property of the model). Vue seems to already send the emit event by default so no need to do it manually everytime.

Collapse
 
branquito profile image
Branchito de Munze

Actually that setter inside computed theModel will never run, which you can easily prove by commenting out that emit statement inside of it, because theModel.foo on input will be changed by mutating that model and not re-assigning over it

Collapse
 
lucasctd profile image
Lucas Reis

Thank you so much buddy!

Collapse
 
blindkai profile image
Blind Kai

I'm happy to be helpful!

Collapse
 
neopixel profile image
NeoPixel.sk

After couple of hours, I found your solution and it works flawlessly! Thank you!

Collapse
 
blindkai profile image
Blind Kai

I'm glad that the article was helpful!

Collapse
 
pleasefilloutthisfield profile image
please-fill-out-this-field

What if I need to validate the value first or make adjustments before emit()ing?

Collapse
 
blindkai profile image
Blind Kai

You could extend the setter.
For example:

const theModel = computed({
  get: () => props.modelValue,
  set: (value) => {
    const isValid = someValidation(value); // Put the logic in setter
    if (isValid) emit('update:modelValue', value);
  },
});
Enter fullscreen mode Exit fullscreen mode