custom components, v-model and values that are object.
See original GitHub issueI am migrating from Vue 1.x to 2.1.x so I am going through my components and painstakingly replacing all my occurrences of :some_data.sync="a"
with v-model="a"
, and adapting the components to accept a value
prop, etc…
My application has lots of complex little input types, so it’s very convenient to create a bunch of custom components that each can handle a specific type and nest these all as needed. I was very happy with how Vue helped me do this in 1.x.
The change from .sync
to v-model
makes sense and I like the idea overall. However most of my custom input components’ value is an object, and that does not seem to be well supported by the new model.
In the example in the docs the value is a string, but if you use the same pattern with an object, the behavior can change considerably!
I’ve lost sight of the long thread where this switch to v-model was discussed but it’s clear one of the main reasons to not use .sync
is you don’t want to change your parent’s data in a child component.
But if my value a
is an object v-model="a"
passes a reference to a
to the child, and any changes the child makes to it affect the parent immediately. This defeats the intended abstraction and makes this.$emit( 'input', ... )
redundant window dressing.
So the solution it seems is for the child to make a deep clone
of the value
prop into a local_val
data attribute whenever it changes. Unfortunately this has to be done when component is mounted
too, which adds boilerplate. (edit: just found out you can clone this.value
right in the data function, so that is a bit cleaner.)
Now when we emit the input
event, we attach our cloned local_val
object which then becomes the value of value
, which then triggers the watch
on value
and causes our local_val
to be replaced by a deep clone of itself. So far that’s inefficient but not inherently problematic.
The real problem is now we can’t use a watch
expression on local_val
to know if it’s been changed (like say in a sub-component) to trigger this.$emit
because you get an infinite loop!
In my case I really wanted to use watch
on local_val
because it’s so much less work than attaching listeners to each of my sub-components (what’s the point of v-model
if I also have to attach listeners everywhere?) So to prevent infinite loops I have to deep-compare this.value
and this.local_val
before emitting an input
. More boilerplate, more inefficiencies.
So in the end my input custom components each have the following boilerplate:
module.exports = {
props: ['value'],
data: function() {
return {
local_val: clone( this.value );
}
},
watch: {
value: {
handler: function() {
this.local_val = clone( this.value );
},
deep: true
},
local_val: {
handler: function() {
if( !deepEqual(this.local_val, this.value, {strict:true}) ) {
this.$emit( 'input', this.local_val );
}
},
deep: true
}
},
//...
So my question is: is that the way v-model
is intended to work with custom input components that have a value
that is an object? Or did I miss something? If I am doing things completely wrong feel free to point me in the right direction.
If so, it seems Vue could do more to help reduce boilerplate and enforce the pattern intended by v-model
. Perhaps it could deep clone data sent to child components via v-model
, and it could do a deep-comparison of data returned on input
event before applying it as a change.
Maybe v-model.deep="a"
could trigger these behaviors?
Thanks for reading!
Issue Analytics
- State:
- Created 7 years ago
- Reactions:30
- Comments:23 (6 by maintainers)
Top GitHub Comments
See https://jsfiddle.net/yyx990803/58kxs8tj/ for an example of how
v-model
is supposed to work with objects.Yes, this is what I do. But I consider that component that holds the primitive values to be input / v-model components as well. And that’s how they end up with an object prop that gets mutated.
There are many examples of simple inputs where the value is an object:
{ length:12, unit:'px' }
{ hours:3, minutes:12: seconds: 58 }
{ x:2, y:4 }
<select>
for that matter){make:'volvo', model:'..', year:2015 }
I seems I should be able to encapsulate such inputs into a reusable component that I can use as simply as this:
<distance-input v-model="css.padding_top">
. Simple and effective and easy to reason about.The problem here, the issue that I am trying to raise is that Vue js 2 tried to fix an anti-pattern and instead gave us an extremely easy way to do a different anti-pattern.
Here is why:
I guess I don’t understand why v-model exists without deep-copying the passed value since there is no way to do what is intended to do without deep copying first.