Sometimes my environment requires components that are stand-alone (i.e. does not rely on a store/pinia of some stripe) but have complex needs in terms of how they are supplied props.
If I have a component in VueJS that takes a v-model that is of type Object:
<ChildComponent v-model:foo="bah" />
let's suppose bah is something along the lines of:
// ParentComponent.vue
<template>
<ChildComponent v-model:foo="bah" />
</template>
<script setup>
const bah = reactive({
foz: '',
baz: -1,
...
})
</script>
we can't do something simple like the samples on https://vuejs.org/guide/components/v-model.html
the usual pattern I take is to deep clone the prop to a local ref (either onMounted or via a watch depending on the situation)
// ChildComponent.vue
const props = defineProps({
foo: { type: Object, required: true }
})
const d_foo = ref(null)
// in the instance that the parent only stipulates foo onMounted
onMounted(() => {
d_foo.value = cloneDeep(props.foo) // lodash/cloneDeep
})
// on some occasions I may expect that foo has been modified before hitting the component, in which case I would use
watch(props.foo, val => {
d_foo.value = cloneDeep(val)
}, { deep: true }) // deep true, if applicable
but this can lead to bloat. Somehow, the component needs to emit to the parent. For example:
watch(d_foo, val => {
emit('update:foo', val)
}, { deep: true }) // if props.foo is being watched, some provisions may need to be made, but that is beyond the scope of this question.
The question, then, is:
if I declare that a child component has say: defineEmits(['update:foo'])
and the parent has v-model:foo="bah"
then why, beyond the needs of convention, should I care about my code policing every emission?
why couldn't I (using the object above) have the following field in my child component:
<input v-model="foo.foz" />
assuming bah has been initialized properly (with ref or reactive) and has an explicit interface (e.g. I defined all its properties when I assigned it to the declaration; I could even have enforced its format as a prop with a validator), data will be mutated on the parent in this context. I've been working now for many years with Vue and I still think this is something of a grey area.
I'm relaying this from a reddit discussion I had on the same subject:
https://www.reddit.com/r/vuejs/comments/1ftnali/comment/lpu5vb5/?context=3
thanks to https://www.reddit.com/user/redblobgames/ for this one:
I think the main reason is that it changes the interface in a subtle way that's not apparent in the interface.
Here's a scenario based on a real-world problem I had (and a simplified vue playground):
We have a . Next year we want to change the parent point representation to be polar, with r: and angle: instead of x: and y:. So we change the parent, and use <ChildComponent :location="polarToCartesian(point)" @update:location="point = cartesianToPolar($event)">. Maybe we use computed() with a setter to encapsulate this pair of conversions.
Under normal Vue conventions where children do not modify props, this change should work fine.
However, if the child mutates props directly, the code will break, because it's modifying the temporary value instead of the actual location. It's not something that's apparent in the interface, which only says that the v-model is a point with x: y:. And the bug may not be noticed right away, depending on how often that value is modified.
an excellent point.
as for alternatives I have found that vueuse has a discrete tool for the job:
const props = defineProps({
foo: {
type: Object,
required: true,
})
const emit = defineEmits(['update:foo'])
const foo = useVModel(props, 'foo', emit, { deep: true, passive: true, clone: true })
this will emit foo changes correctly even where a local field has modified a single property of the object (it uses a watcher under the hood so the usual caveats apply around huge objects etc.)