I 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!
Hi, thanks for filling this issue.
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.
Actually, if you don't want to make it affect parent immediately, you should not be mutating the prop object directly in child. Any change to the prop should be done by $emit('input', <mutated clone>)
, which is how v-model
was designed to be used with objects. - Yes, the parent state gets replaced by a new fresh object.
In your case, using a local cloned value is the right approach. It serves as a temporary state of the child, and it should be flushed by parent state changes triggered by $emit('input', ...)
Therefore, simply removing the $emit
inside local_val
watcher should do it. Only $emit
elsewhere, when you want to update parent state and flush the local state. This way you can still watch local_val
and react to changes.
Also, I really think that maybe you should consider breaking the component down if it's value
is a deep object - multiple components modifying primitive values / simple objects like {a:1}
makes it easier to maintain and reason about.
I really think that maybe you should consider breaking the component down if it's value is a deep object - multiple components modifying primitive values / simple objects like {a:1} makes it easier to maintain and reason about.
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.
The way I look at props
is I'm passing an argument by value, not by reference. To me, doing this.value = something
(where value
is a prop) inside a method, for example, doesn't make sense, but this.value.someProp = someValue
does.
Thinking about it as someone who's not privy to the internals of Vue, it seems logical/safe to modify a prop object's property since the parent component and any other component that depends on that same object for its state can react accordingly. But changing the object itself in a child component would imply somehow updating all those references magically.
I would asume this is one of the motivations for the data down/actions up paradigm. I myself probably wouldn't worry about deep cloning a prop object and using $emit
for every change.
I'm with teleclimber, I've read the docs several times and cannot find my way around this issue.
@teleclimber I'm guessing clone and deepEqual are npm packages?
See https://jsfiddle.net/yyx990803/58kxs8tj/ for an example of how v-model
is supposed to work with objects.
@yyx990803: Correct me if I'm wrong: I believe using v-model
is conceptually akin to passing arguments to a function which has a return value which itself is a result of a calculation based off of those same arguments. v-model
is your argument and the component is your function; $emit('input', newValue)
is essentially returning the new value to your caller (the parent component).
Of course, not all functions need to return a value. Some can operate as a procedure and simply modify your program's state in some way, possibly even the same function's arguments. This could be the distinction between using v-model
or just plain v-bind:value
.
Since Javascript uses the call-by-sharing evaluation strategy for function arguments, it is possible to modify arguments' properties inside a function, and have those changes visible outside it without returning anything.
E.g., you wouldn't normally do:
function modify(a) {
const b = deepCopy(a) // possible bottleneck
b.someProp = 'someValue'
return b
}
You'd just do:
function modify(a) {
a.someProp = 'someValue'
}
modify(someVariable)
someVariable.someProp === 'someValue' // true
I, personally, don't find it necessary to clone my prop and $emit
every time I change something about it (which can happen in many ways and many times). It introduces more complexity (since you have to eventually use watchers that handle the input
emits in a DRY way and then you start having the problems @teleclimber mentioned), possible performance issues, and there's no benefit to it. IMO, there's not even a benefit of correctness.
Now, the true answer here might be that v-model
is probably not appropriate for objects, just for primitive values. For objects, just use v-bind:value
.
@rhyek FYI, if I am writing a function that returns some value, I'd avoid mutating the object passed in if possible. A function that mutates its arguments produces side effects when called and is thus always impure. This makes them harder to reason about:
function one (obj) {
return {
...obj,
someProp: 'someValue'
}
}
function two (obj) {
obj.someProp = 'someValue'
}
var newValue = one(oldValue) // no side effects
two(oldValue) // side effect: oldValue is mutated
The point here is calling two
on oldValue
can cause unexpected behavior if other parts of your code has logic that depends on oldValue
and assumes it has not changed.
As for v-model
, the benefits of cloning an object is making the values immutable, similar to primitive values like numbers and strings. Sticking to this practice allows you to reason about all v-model
behaviors consistently no matter what the the data type is: the value you pass in never gets mutated, when an update happens, a new value gets sent back and replaces the old value. (This also means you don't need to use deep watchers.) This consistency is a clear benefit to me, making things easier to reason about. Cloning objects are not complex by any means with Object.assign
or better spread operators, and Arrays provide immutable methods already. As for performance... v-model
updates are very, very low frequency operations compared to other parts of the framework, so it's practically negligible. That said, you are free to ignore the advice from me if you insist :)
@yyx990803: I understand your reasoning and believe it to be sound. Only:
The point here is calling two on oldValue can cause unexpected behavior if other parts of your code has logic that depends on oldValue and assumes it has not changed.
This is true, but doesn't really happen in VueJS land, since all my code is always using the newest version of my object anyways.
Sticking to this practice allows you to reason about all v-model behaviors consistently no matter what the the data type is
This is the only real reason for cloning prop objects: cognitive consistency.
Anyways, thank you for your comments.
@yyx990803, thoughts on deep vs shallow copy in this context?
@rhyek I'd consider it a misuse if v-model
is used with an object more than 1 levels deep. That essentially becomes shared state. I'd just use a proper state management pattern instead.
I'd consider it a misuse if v-model is used with an object more than 1 levels deep. That essentially becomes shared state. I'd just use a proper state management pattern instead.
@yyx990803 What about in the case of a date / time multiple select box.
I this scenario I see two sub-components, one for date and one for time. You could even take this a step further and say each of the select boxes is a <vue-select>
custom component.
How would you handle this situation?
@yyx990803 I'm also interested in what the recommended pattern is here. I work on a component library, and it is very common to compose reusable components into other (more complex) reusable components.
@leevigraham I don't think that is related to components with objects as their value. If I'm reading it correctly it simply allows customizing the property and event name.
How can I use v-model for two level deep nested component?
e.g.
HTML
here day
is an object
<opening-hr-field v-model="day"> </opening-hr-field>
JS template
````
24 hour
v-on:input="$emit('input', $event.target.value)">
Here, I have two level deep v-model. Can you please check whether it's right way to do? How can I propagate the emit from 2nd template to first template and all way up to the parent?
This seems like a common enough use case to justify some kind of built-in helper?
Google brought me to this thread while searching for a better understand of the props/emitter pattern. I was particularly unsure about emitting an event using v-model
from a deeply nested component to the top. I think this might be the cleanest solution I found:
// MyComponent.vue
export default {
props: ['value'],
computed: {
localValue: {
get: function() {
return this.value
},
set: function(value) {
this.$emit('input', value);
}
}
}
});
This can be used from the parent as follows (where whatever
could also be a computed property defined in the same manner above):
<MyComponent v-model="whatever"></MyComponent>
Within the component all other setter and getter operations should use the localValue
computed property instead of the value
prop. This makes updating and emitting a change to the parent easy:
this.localValue = 'new value'
If value
is an object then it might be worthwhile creating computed properties for those as well. For example, if value
represented a "person" with firstName
and lastName
properties:
// MyComponent.vue
export default {
props: ['value'],
computed: {
localValue: {
get: function() {
return this.value
},
set: function(value) {
this.$emit('input', value);
}
},
firstName: {
get: function() {
return this.localValue.firstName;
},
set: function(value) {
this.propertySetter('firstName', value);
}
}
},
methods: {
// mixin?
propertySetter: function(key, value) {
this.localValue = Object.assign({}, this.localValue, {[key]:value});
}
}
});
This allows firstName
and lastName
to be used with v-model
from MyComponent.vue
, and have changes propagate all the way up. E.g.,
<input type="text" v-model="firstName" />
I'm still experimenting with this but it seems to be the most consistent approach I could find.
Forgive me if this is stupid, but why even bother with v-model
? Using just a prop
, everything works as expected. https://codepen.io/anon/pen/jdgMpq
Forgive me if this is stupid, but why even bother with
v-model
? Using just aprop
, everything works as expected. https://codepen.io/anon/pen/jdgMpq
The concern with your example is the modification of the todo
prop within your component. This goes against the Vue design pattern. You can read more about this in the Vue.js documentation on One-Way Data Flow.
@chriscdn thanks for the write-up. I found a case where the approach breaks down though: https://codepen.io/anon/pen/OGEaRg?editors=1010
If you have a method that updates multiple properties sequentially, only the last update wins. I believe this is because $emit
is asynchronous and the value
isn't updated by the time the next Object.assign()
occurs. Any advice?
@timwis Well spotted! I would have assumed it would be synchronous. No advice, but I'll give it some thought.
I'm surprised there isn't an official solution for this.
Here was my take:
export default {
props: ["value"],
computed: {
proxy() {
const pendingChanges = {};
return new Proxy(this.value, {
get: (target, key) => target[key],
set: (target, key, newValue) => {
pendingChanges[key] = newValue;
this.$emit("input", { ...target, ...pendingChanges });
return true;
}
});
}
}
}
Everything goes through the proxy
object:
<input type="text" v-model="proxy.firstName" />
<input type="text" v-model="proxy.lastName" />
@achaphiv because proxies aren't recursive. Your example works because it is a flat object, sure, but if it's a flat object then it isn't asking much to use a computed value with a get()
and set()
function.
For those considering the recursive proxy route, let me save you from going down the rabbit hole: To make matters worse with nested objects, recursive proxy wrappers (aside from over-complicating things with even more boilerplate) will again mutate the original object. For example:
export default function deepProxy(model, setTrap, propPath = []) {
function get(target, prop) {
// Respond to checks if this is a proxy to avoid double-wrapping
if (prop === '$$deepProxy') {
return true
}
if (target[prop] && typeof target[prop] === 'object') {
if (target[prop].$$deepProxy !== true) {
target[prop] = deepProxy(target[prop], setTrap, propPath.concat(prop))
}
}
return target[prop]
}
return new Proxy(model, { get, set: setTrap(propPath) })
}
Evan's skeleton example applies for nested objects as well. For every nested object that you need to modify individual properties on, you should probably be passing that sub-object into a sub-component to handle the responsibility of that data and then the pattern can scale as many level deep as you need it to.
Most helpful comment
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.