question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

custom components, v-model and values that are object.

See original GitHub issue

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!

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Reactions:30
  • Comments:23 (6 by maintainers)

github_iconTop GitHub Comments

32reactions
yyx990803commented, Feb 14, 2017

See https://jsfiddle.net/yyx990803/58kxs8tj/ for an example of how v-model is supposed to work with objects.

28reactions
teleclimbercommented, Dec 4, 2016

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:

  • a distance + units { length:12, unit:'px' }
  • time: { hours:3, minutes:12: seconds: 58 }
  • position: { x:2, y:4 }
  • any picker that can select multiple values (like <select> for that matter)
  • car {make:'volvo', model:'..', year:2015 }
  • and on and on…

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:

  1. v-model exists to create input components, right? So the prop passed in v-model is intended to be modified, right? That’s the only reason to pass things via v-model.
  2. Because Vue js doesn’t use immutable data, and it passes everything by reference, modifying the prop passed is an anti-pattern. (“This means you should not attempt to mutate a prop inside a child component.” )
  3. So if I modify the value prop that I got with the intention of modifying it, I’m doing the anti pattern. How does that make sense?

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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Vue.js: Using v-model with objects for custom components
js provides an example how v-model is supposed to work with objects. Basically every data update of the component has to $emit a...
Read more >
How To Add v-model Support to Custom Vue.js Components
Thankfully, Vue allows us to customize it. To customize the event and ``prop, we add a model property to our component and define...
Read more >
Using Vue's v-model With Objects - Drew Town Dev
I'll occasionally run across a scenario where I have an object or a list of objects that would be useful to extract into...
Read more >
Making sense of Multiple v-model Bindings in Vue 3
The Component handles the state internally for all the input elements. It generates a single payload object representing the state of the ...
Read more >
Using v-model in Vue 3 to build complex forms - LogRocket Blog
In the custom component, the v-model directive assumes an internal ... object would be generated to represent the state of the component.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found