1

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.

asked Oct 1, 2024 at 11:05
15
  • 3
    Please, provide stackoverflow.com/help/mcve that reflects the expected relationship between the components and their states. What you describe can look a few different ways. You can do <input v-model="foo.bah.foz" in a child if foo is child's own state, e.g. it's deep cloned from a prop. And you can't do this if foo is a prop, because this would result in mutating parent's state in a child, which is a bad practice Commented Oct 1, 2024 at 11:37
  • no problem, updated. Commented Oct 1, 2024 at 12:01
  • 1
    If you don't need child's own state for other reasons, the alternative is to use custom events instead of v-model to partially mutate the parent's state in a parent. Because deep cloning on every change can become inefficient with large state. E.g. stackoverflow.com/a/79028365/3731501 and stackoverflow.com/a/78941552/3731501 Commented Oct 1, 2024 at 12:19
  • 1
    Yes, that's correct. "foo" should be cloned foo prop, i.e. d_foo in your case. Btw what's about the name with d_ prefix, is this some convention? I saw it already in third-party libs. Commented Oct 1, 2024 at 12:23
  • 2
    @Sanderr A macro can be expressed manually with a writable computed, e.g. stackoverflow.com/a/79009268/3731501 , this is ok for <input v-model="foo"> but not for <input v-model="foo.bar"> because nested properties aren't handled by computed's "set" trap Commented Oct 1, 2024 at 15:28

2 Answers 2

1

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.)

answered Oct 2, 2024 at 16:01
Sign up to request clarification or add additional context in comments.

9 Comments

the both solutions in your answer don't offer anything new that wasn't already discussed and rejected. 1. in the reddit solution the object is cloned 2. in useVModel solution the object is deeply mutated (which is bad) and just an extra update:model event added sending already mutated object.
Propose an alternative to cloning the prop/the above please. If you can think of a succinct alternative without a heap of boilerplate then you should post an answer here and - quite honestly - blog it. Remember, this is for stand alone components that do not use vuex/pinia/provide|inject etc. I will investigate vueUse's useVModel now.
incidentally, yes the above requires the clone option set to true. that's amended (you can also supply lodash's cloneDeep etc.) What is your objection to cloneDeep? It's going to depend on your use thereof. I wouldn't use it in all instances but for relatively small objects (even with 2 or 3 dimensions and several properties in each) it is perfectly performant. Stand alone components aren't going to need humongous objects; probably shouldn't do either really.
yes, i've mentioned that v-model for small plain objects only in my answer, but deleted the answer. Cloning v-model for complex objects isn't a solution since an object could be a complex class instance with circular references so you cannot just simple copy it
Yes, I take your point; if one were using typescript you could specify that the object have specific properties and enforce against complex classes with circular references. Either way, my discussion is stand alone components; such things ought to be properly documented with/without ts. If this is a component available to 3rd party developers: the vueUse clone method uses JSON.parse(JSON.stringify) which will safely handle circular JSON with an exception allowing you to feedback to the client.
|
-1

Can you just take the object key as a props?

// Paren.vue
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue';
const bah = ref({
 foz: '',
 baz: -1,
})
</script>
<template>
 {{bah}}
 <ChildComponent v-model="bah" modelKey="foo" />
</template>
// Child.vue
<script setup lang="ts">
defineProps<{
 modelKey: string;
}>();
const model = defineModel()
</script>
<template>
 <input v-model="model[modelKey]"/>
</template>

Vue SFC Playground

answered Oct 2, 2024 at 20:17

7 Comments

unfortunately, that will mutate the model-value prop pop the following into the app.vue of your playground (apologies for no spacing, you'll need to \n it): <script setup lang="ts"> import { ref } from 'vue' import Child from './Child.vue'; const bah = ref({ foz: '', baz: -1, }) const observed = ref(false) const observeMe = () => { observed.value = true } </script> <template> {{bah}} <Child :model-value="bah" @update:model-value="observeMe" model-key="foo" /> {{ observed }} </template>
It would be more visual to create an example on the same Vue SFC Playground. It's not difficult. Because I don't quite understand what you are talking about.
I wanted to but I could't as it's too long for the comments. Here's the correction shortened with some extra deets: shorturl.at/Hkbl2 Observe that the event never triggers.
I think I finally understand your question and what you want )) shorturl.at/kS3rH
As a rule you shouldn't ever mutate props, even if you're emitting as well. So once again, unfortunately, that is incorrect. defineModel is not designed to handle objects and the VueJS docs need to do a better job of teaching 2-way object management. there are numerous pitfalls. My solution above is my personal preference but, yes, requires cloning; thus, should only be used sparingly where the object would be a nuisance to manage with props.
|

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.