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.