New composable and directive for parent to child object mutations: useDeepModel and v-deep-model #751
Closed
michaelcozzolino
started this conversation in
RFC Discussions
Replies: 1 comment 1 reply
-
Hi @michaelcozzolino, any news? why was this closed? |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Edit: After many attempts in trying to make a draft for the directive I have to say that, at least to me and my knowledge, it's almost impossible to achieve it, as working with references(objects) will always end up in mutating the original object directly through the directive, even destructuring recursively and assigning the new value ended up in side effects when working with more than 1 input field. Same as for the composable, it works good if used in a non recursive way, meaning that you have to pass into it only the root object, if you want to edit a sub object you will have to create another component and pass through it the sub object, because passing the sub object directly in the composable and working with that might still mutate the property of a prop without realizing it, and it's very difficult if not impossible to ensure that it won't happen. So, to conclude, if you need to mutate deeply nested object properties through props, the best option is to go with a pinia/vuex store and there will be neither problems with reactivity nor with vue props mutation violations.
Summary
At the moment Vue does not provide any way to update the property of an object passed through props by using v-model
without breaking the parent -> child data flow. Even by using defineModel in the child component as in the following:
it would break the data flow, because no event is emitted, hence the data is updated because the object is a reference.
(https://vuejs.org/guide/components/props.html#mutating-object-array-props)
The goal is to ideally have a composable and a directive that are capable of working with objects deeply.
Basic example
Composable (useDeepModel)
directive (v-deep-model)
Motivation
Many times developers end up in having complex and big objects where the UI to build should manipulate and show their
data. It could be easy to work by just putting all the data in inputs in the same Vue component, so that the whole
data manipulation workflow doesn't need to update parent components where the data is passed to children.
This approach would yes, work, but at the end it would make the whole codebase to be complex and tricky to understand, so
developers start splitting the code in multiple components to handle the data being stored in one or multiple objects,
this requires the usage of two-way data binding approaches such as props + emits + assignment in the parent or
almost full automation of the former with
defineModel
, that even if it might seem appetizing, it lacks the possibilityof the partial object update so that if you have:
this cannot be considered completely valid as there is no emitted event, instead what can be considered valid could be:
having these kinds of assignations makes the code verbose and complex to read if the object is big enough, as the developer
will have to replicate the same for every object property.
Detailed design
Both composable and directive, must work in a way that the parent component uses
<Child v-model="object" />
Composable (useDeepModel)
The idea is to have a composable accepting the modelRef from the child (
const modelRef = defineModel<Type>()
)and returning a writable computed having a proxy whose setters automatically sets the new property through a complete
object update, and the setter of the writable computed will set the new object to the modelRef letting it automatically
emit the event related to the update (similar to what is explained here https://skirtles-code.github.io/vue-examples/patterns/computed-v-model):
The composable above is a simplified version of the final one that should handle also object properties in arrays, to
achieve this case the solution is to return a deep proxy, something like this:
Directive (v-deep-model)
The idea of this directive is that it should be a direct extension of
packages/runtime-dom/src/directives/vModel.ts,
meaning that it would reuse the code from each hook of the standard
vModel
as thevDeepModel
should be used to stillupdate primitives with the difference that they are direct properties of an object, for this purpose I came up with
the following api to be used in the child:
Definitions:
Deep mutation
: the update/mutation of a property of an object through a whole object update after that an eventhas been emitted for that purpose.
Notations
Object notation
v-deep-model:arg="value"
Array notation
v-deep-model:arg="[objectToUpdate, indexKey]"
Notation differences and properties
value
isobject|[object, number]
, in the first case we have theObject notation
whosevalue
isexactly the object to deeply update; in the second case we have the
Array notation
whosevalue[0]
is the objectcontained in the array to update at a specific index being
value[1]
or the array to update at a specific index beingvalue[1]
.In js arrays are considered objects so basically indexKey should be
number|string|symbol
, but I would restrict the typethere to
number
just to have a neat distinction between objects and arrays to deeply mutate, so all the cases whereArray.isArray(value[0]) === true
.arg
is always required and there can be multiple ones, separated by a column:
and it always refers to oneor additional child properties of
value
:v-deep-model:first-arg:second-arg="value"
->value.firstArg.secondArg
will be updated.v-deep-model:second-arg="[value.first-arg, index]"
->value.firstArg[index].secondArg
will be updated.v-deep-model:first-arg:second-arg="[value, index]"
->value.firstArg[index].secondArg
will be updated.The
Array notation
is pretty useful when working with objects being elements of array properties, and it cannot containmore than 2 elements.
The
Object notation
is a sub notation of theArray notation
meaning that you can still replicate the same behaviourof the former through the latter but not viceversa.
Examples
Object notation
v-deep-model:name="editablePerson"
: It means that the objecteditablePerson
will be deeply mutated on thename
property.v-deep-model:address:city="editablePerson"
: It means that the objecteditablePerson
will be deeply mutated on theaddress.city
property,where
address is an object
andcity
its property.v-deep-model:city="editablePerson.address"
: It is the same as 2 with the difference that theaddress
objectis the value instead of an argument.
Array notation
v-deep-model:name="[editablePerson]"
: It is the same as 1.v-deep-model:children:name="[editablePerson, index]"
: It means that the objecteditablePerson
will bedeeply mutated on the
children[index].name
property.v-deep-model:name="[editablePerson.children, index]"
: It is the same as 5.Drawbacks
This feature obliges the user to use
defineModel
and implicitly, in case of (many) nested components, it might leadto prop drilling. I still believe that having 3-5 nested components and manipulating the data this way is absolutely fine
and that amount of components would be the average used by the developers, and it's actually better than attempting to
mutate a deep reactive property of a prop object. Vue itself is suggesting the use of defineModel, so the usage of
the combination of defineModel and, useModel or v-deep-model, is perfectly acceptable.
Alternatives
At the moment the only alternatives are:
https://vueuse.org/core/useVModel/#usevmodel
https://vueuse.org/core/useVModels/#usevmodels
But they might be expensive in terms of resource usage as they use (deep) watchers.
Adoption strategy
It's a completely new feature that adds a new composable and a directive that is built on top of an existing one, so no
breaking change should be involved.
In case my proposal gets approved I'm up to implement it.
Unresolved questions
N/A
Beta Was this translation helpful? Give feedback.
All reactions