Listening to external programmatic model mutations with defineModel
#11250
8ctavio
started this conversation in
General Discussions
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Hi, I'd like to discuss how/why to listen to a custom component's model external programmatic mutations.
When authoring a form field component with
defineModel
, new values for the component's model may be obtained from two sources:When a new value is obtained, additional operations may be required before or after updating the component's model. Moreover, these operations might vary depending on the new value's source.
Two examples
1. Calendar component
Consider a Vue component to wrap a package's Javascript-only calendar. The package's API allows to define event handlers where the model value can be updated according to the calendar's selected date. However, if the parent mutates the model value, it would be required to update the calendar's state to reflect that change. This is only the case for external changes since internal changes due to calendar interaction automatically update calendar's state.
2. Number input component
Consider a Vue component for a number input whose model value is of type
number
but the underlying input element'svalue
is a formatted version of the number. Every time the input'sinput
event is emitted or the parent mutates the model value, the formatted number version must be computed.The problem
In order to properly handle internal and external changes it is required to independently listen to changes from each source. Listening to internal changes is straightforward; usually achieved through event handlers for mounted elements. It is not completely clear, however, how to properly listen for external changes.
Using
watch
Since the
model
obtained fromdefineModel
is aref
(synced with the parent), an option to detect external mutations is simply usingwatch(model, onExternalUpdate)
. However, with this approach there would inevitably be additional overhead since, by design, themodel
is not exclusively mutated externally but also internally as a result of either internal or external changes.model
updated as a result of internal changesWhen
onExternalUpdate
is called due to a localmodel
update, there are two possibilities:onExternalUpdate
run (i.e., do nothing). The execution ofonExternalUpdate
due to localmodel
updates might not produce adverse side effects. However, unnecesary operations would always be performed every timemodel
is internally updated (see example 1).onExternalUpdate
operations, aninternalModel
ref
might be implemented to handle internal changes. WheninternalModel
is updated,model
should be updated accordingly. Then, inonExternalUpdate
, ifmodel.value === internalModel.value
it may be assumed that the change was produced internally, so the callback operations can be skipped. Of course, for some applications, this assumption may prove to be insufficient. Furthermore, for some model values, testing for equality is not so straightforward.model
updated as a result of external changesSometimes,
model
might be further updated insideonExternalUpdate
. This will rerun the callback. Again, there are two possibilities:onExternalUpdate
run (i.e., do nothing). If the re-execution ofonExternalUpdate
updatesmodel
with its current value no further adverse side effects are produced. Again, the problem is performing unnecesary operations before themodel
is re-updated (see example 2).model
value might be sufficient to skip callback operations. For example, if in the first callback execution the model is formatted, at the second execution it may be verified if the model is already formatted.In short,
onExternalUpdate
will be also called for internal model updates. These additional calls will either perform unnecesary work or require additional verificaction to skip operations. In any case, overhead is present.Even though functionality is achievable and in some cases overhead might be neglected, using
watch(model, onExternalUpdate)
simply does not seem to be the right approach.Fundamental problem
The fundamental problem appears to be the way the component's model is defined. Currently, a custom component's model is defined in its parent. The parent is then responsible for providing the component's model to the component itself.
Using
v-model
seems to imply a parent's intent for the component to take full control over the model (at least for cases such as a form field component). Therefore, it would make sense to define the component's model in the component, and expose a read-only version and an update function for the parent to interact with the custom component's model. This way, the custom component could easily identify external mutations.Below are three alternatives to listen to external programmatic model mutations.
1. Expose model with
defineExpose
As previously described, an alternative is to define the model in the custom component and expose it with
defineExpose
.Note that the component's model definition is analogous to defining
modelValue
andupdate:modelValue
props (or usingdefineModel
), but now the component has more control over how to update its model value.However, the API to interact with the custom component's model becomes less convenient. A
useModel
composable to consume the exposed model may be introduced to improve API.See full example.
Even though the concept of exposing the model from the custom component seems like a good approach, its implementation with available APIs present some drawbacks, so a better implementation is sought.
2. Provide function for component to register callback
Although conceptually there may be better approaches to defining a component's model in its parent, in practice it is by no means a limitation.
As seen in the previous alternative, it is desired for the component to be able to define a function to handle external programmatic model mutations. The parent could provide a function for the component to register a callback and use it to update the component's model.
The suggested API is presented below.
For the parent the API is similar to using
v-model
. Model modifiers could be implemented as well, but they could not be so conveniently specified. For the component, the API requires some extra steps compared to only usingdefineModel
.See full example.
3. Provide custom object through
v-model
(modelValue
prop)Lastly, in order to reduce additional steps required for using component models, an object containing
may be sent directly as a prop. For convenience,
v-model
could be used to send the object in amodelValue
prop; theupdate:modelValue
prop should be ignored.An additional composable analogous to
defineModel
may be implemented to properly consume themodelValue
prop.The suggested API is presented below.
See full example.
Beta Was this translation helpful? Give feedback.
All reactions