Your initial problem is quite simple. In Vue 3 v-model
defaults to to a prop called modelValue
and emits come from update:modelValue
. Other answers here have assumed that in their solutions but not directly addressed it.
You can either rename your messageObj
prop to use the default prop OR use the multi-model features in Vue 3:
<Message v-model:messageObj="message" />
However our problems run deeper.
All (current) answers will work but aren't quite correct. They all fail the idiomatic "One-way Data Flow" rule.
Consider this JSFiddle, modified from this answer.
const child = {
template: `<input v-model="message.test" type="text" />`,
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue,
// No set() ?
});
return { message };
}
}
In this example, the child component never 'emits' - yet the data is still updating in the parent component. This violates the "One-way" rule. Data must be propagated from child components using only emits and not via prop proxies.
The problem in here is that props.modelValue
is reactive when arrives in the child component. One can verify this with the isReactive()
helper. When it's passed through the computed()
it retains that reactiveness and will continue to proxy updates through itself into the parent component.
A solution:
JSFiddle here
const { createApp, ref, computed } = Vue;
const child = {
template: `<input v-model="message" type="text" />`,
props: {
modelValue: {
type: Object,
default: () => ({}),
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const message = computed({
get: () => props.modelValue.test,
set: (test) => emit('update:modelValue', ({...props.modelValue, test })),
});
return { message };
}
};
createApp({
components: { child },
setup() {
const message = ref({ test: 'Karamazov' });
return { message };
}
}).mount('#app');
The solution is three parts:
The computed getter must not return the proxy object from the parent component. Once this happens you're in danger of violating the "one-way" rule [note 1]. In this example props.modelValue.test
is a string so we're safe.
The computed setter must emit the whole object, but again it must not be a reactive type. So we clone the modelValue
using spread and include in the updated test
field. This can also be achieved with Object.assign({}, props.modelValue, {test})
[note 2].
The message
variable in the parent component cannot be a reactive()
and must be a ref()
. When the v-model
receives the newly emitted object the message
variable is clobbered and no longer reactive [note 3]. Even with refs the props.modelValue
will still fully reactive when it arrives in the child component, so the cloning steps are still important.
Alternatively:
I should also mention that values from computed()
are not deeply reactive. As in, setting values on a computed object will not trigger the computed setter.
An alternate solution for passing the whole object through to your template:
setup(props, { emit }) {
const message = reactive({...props.modelValue});
watch(message, message => emit('update:modelValue', ({...message})));
return { message };
}
In this, the whole message
object will emit whenever the .test
field is updated. E.g. <input v-model="message.test" />
. This still obeys the "one-way" data rule because emits are the only way data is given to parent component.
Reasoning:
"One-way" data flow is important [4]. Consider this:
<child :modelValue="message"></child>
On a first (and a sensible) glance, this appears to pass data into 'child' but not out of 'child'. But, given a reactive object that is not handled by the child correctly, this will emit changes into my own component.
Observing this code I don't expect this behaviour so it's very important that the child component gets it right.
Notes:
[1]: Testing violations of the "one-way" rule are surprisingly simple. Remove any emit
and if the parent receives updates - you've broken it. Or replacing v-model
with v-bind
also works.
[2]: Object.assign()
and {...}
spread are indeed different. But shouldn't affect our uses here.
[3]: I haven't found any clear documentation about this behaviour regarding reactive()
and v-model
. If anyone wants to chime in, that'd be great.
[4]: The Vue docs stress the importance of one-way bind. Evan himself (creator of Vue) even provides examples about how to use v-model
with objects (in Vue 2, but the principles still apply).
I feel it's also important to note later in the same thread Evan suggests objects that are nested more than 1-level are considered misuse of v-model
.
{{ message.test }}