Skip to content

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

@teleclimber

Description

@teleclimber

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!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions