Not sure how to properly approach defineModel with objects #10538
-
Beta Was this translation helpful? Give feedback.
Replies: 12 comments 10 replies
-
| Up | 
Beta Was this translation helpful? Give feedback.
-
| I have the same problem (I don't have an ideal solution but I can emit events..). Just like you, I want to define an Object type model with defineModel, but using defineModel like this: export type Model = {
  a: string;
  b: string;
}<script setup lang="ts">
import GrandChild from "./GrandChild.vue";
import type { Model } from "./type"
const model = defineModel<Model>()
</script>
<template>
  <div>
    <h3>ChildIdeal Component</h3>
    a: <GrandChild v-model="model.a" /><br />
    b: <GrandChild v-model="model.b" />
  </div>
</template>(GrandChild is a simple wrapper for input. See the link above.) GrandChild's v-model does NOT emit Child's update:modelValue so it means the object(=prop) is mutated. In Vue devtools, there is only one emit which is from GrandChild. So, I had to write like this: <script setup lang="ts">
import GrandChild from "./GrandChild.vue";
import type { Model } from "./type"
const model = defineModel<Model>()
const onUpdateA = (a: string) => {
  model.value = {...model.value, a}
}
const onUpdateB = (b: string) => {
  model.value = {...model.value, b}
}
</script>
<template>
  <div>
    <h3>ChildActual Component</h3>
    a: <GrandChild :model-value="model.a" @update:model-value="onUpdateA" /><br />
    b: <GrandChild :model-value="model.b" @update:model-value="onUpdateB" />
  </div>
</template>In Vue devtools, there are two emits which are from Child and GrandChild. Declaring and using onUpdate function and binding model-value / @update:model-value is a tricky and reluctant solution... We need a more simple and clear solution. | 
Beta Was this translation helpful? Give feedback.
-
| Is mutating the prop value not considered bad practice anymore? If the latter, I think a lot of buggy code will be written. I assumed that, internally, defineModel would call an "update:modelValue" event, with an updated copy, whenever we change a property value . I think a lot of other people assume the same. Maybe defineModel should return ModelRef< | 
Beta Was this translation helpful? Give feedback.
-
| I would also like to use  To avoid these pitfalls should I do something like this? Not saying this is the solution, just wondering if this would avoid the problem until we have something better. And here is the code from that example: <template>
  Full Name Input in Child: <input v-model="localForm.name.fullName" />
</template>
<script setup lang="ts">
import { reactive, watch } from "vue";
// Making a form object with nested properties
interface ContactForm {
  name: {
    fullName: string
  }
}
// Creating the parnet/child component v-model sync using defineModel
const contactForm = defineModel<ContactForm>({
  required: true,
});
// Making a copy of contactForm to avoid mutating it's properties
const localForm = reactive<ContactForm>({ ...contactForm.value });
// Watch for changes in localForm and update the contactForm to keep it in sync with parent
watch(
  () => localForm,
  () => {
    contactForm.value = { ...localForm };
  },
  { deep: true },
);
</script> | 
Beta Was this translation helpful? Give feedback.
-
| Up, having the same issue | 
Beta Was this translation helpful? Give feedback.
-
| I caught this myself when tried to use shallowRef in parent. Vue documentation should be updated I was absolutely certain that define model creates a reactive copy of the passed prop. So that when I did modelValue.someField = value it would emit the new copy of the object not perform the actual direct mutation. | 
Beta Was this translation helpful? Give feedback.
-
| I have discussed this issue on a stack overflow. The best suggestion I can make at this point is to use the similar but more feature rich  I show a strategy over here: https://stackoverflow.com/questions/79042840/vue-v-model-with-objects#answer-79047600 in brief: const emit = defineEmits(['update:foo'])
const foo = useVModel(props, 'foo', emit, { deep: true, passive: true })this emits correctly when changing deep properties of an object/array. it uses watchers under the hood so take that into consideration. | 
Beta Was this translation helpful? Give feedback.
-
| It's a real shame there's no out of the box support for this, I ended up doing this The tab-ing between elements is still glitchy but it's working even if I shouldn't modify props. | 
Beta Was this translation helpful? Give feedback.
-
| I made a proposal for supporting it out of the box (with a solution which you can copy-paste): vuejs/rfcs#725 | 
Beta Was this translation helpful? Give feedback.
-
| Is there any progress to use defineModel with objects out of the box? | 
Beta Was this translation helpful? Give feedback.
-
| For me, it seems that I also encountered the same problem. Take the example of @michaelcozzolino as an explanation <template>
  <div>
    <input name="person-name-input" v-model="name">
  </div>
</template>
<script setup lang="ts">
import type { Person } from './types';
const person = defineModel<Person>({ required: true });
const name = computed({
  get: () => person.name;
  set: (newName: string) => person.value.name = newName;
});
</script>We can have <template>
  <div>
    <input name="person-name-input" v-model="name">
  </div>
</template>
<script setup lang="ts">
import type { Person } from './types';
import { toRefs } from '@vueuse/core'
const person = defineModel<Person>({ required: true });
const { name } = toRefs(person, { replaceRef: false /* important! */ })
</script>
 Then equivalently, we can implement the following  /**
{
  "msg": {
    "content": {
      "text": "xxx"
    }
  }
}
*/
const msg = defineModel('msg');
function toRef(target, key) {
  const _ref = customRef(() => {
    return {
      get() {
        return target.value?.[key];
      },
      set(v) {
        if (!target.value) target.value = { [key]: v };
        else target.value[key] = v;
      },
    };
  });
  return _ref;
}
const content = toRef(msg, 'content');
const text = toRef(content, 'text'); | 
Beta Was this translation helpful? Give feedback.
-
| By the way, there is a less conventional way of writing it. If the entire modelValue is replaced, it will only retain the last assignment operation during the continuous assignment process. Let's borrow @kesoji example. play // ChildIdeal.vue
const model = defineModel<Model>()
setTimeout(() => {
  model.value = { ...model.value, a: '111' } // not work
  model.value = { ...model.value, b: '222' } // I got { "a": "a value", "b": "222" }. but expect { "a": "111", "b": "222" }
})The specific reasons are reflected in this operation. packages/runtime-core/src/helpers/useModel.ts function useModel(props, name, options = EMPTY_OBJ) {
  const i = getCurrentInstance();
  return customRef((track, trigger) => {
    let localValue;
    watchSyncEffect(() => {
      const propValue = props[name];
      if (hasChanged(localValue, propValue)) {
        localValue = propValue;
        trigger();
      }
    });
    return {
      get() {
        track();
        return options.get ? options.get(localValue) : localValue;
      },
      set(value) {
        i.emit(`update:${name}`, options.set ? options.set(value) : value);
      },
    };
  });
}
const defineModel = useModel;If we simplify it a little, we will find that it is actually the useModel function that synchronizes the changes of props through watchSyncEffect. Therefore, when we modify modelValue as a whole, the localValue hidden inside useModel will not be modified at all. This is why the assignment of modelValue twice in a row only takes effect for the last time. So we made a slight modification and waited for a micro-task, which would enable us to achieve the desired result. setTimeout(async () => {
  model.value = { ...model.value, a: '111' }
  await Promise.resolve(res => queueMicrotask(res))
  model.value = { ...model.value, b: '222' } // I got { "a": "111", "b": "222" }
}, 500) | 
Beta Was this translation helpful? Give feedback.


No. We have 2 options: mutating nested properties of props directly or updating reference to the defineModel's model by shallow copying the original model and reassigning it.
I prefer the 1st option since it is cheaper and less boilerplate'y though against vue principals of not mutating props.