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
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
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
numberbut the underlying input element'svalueis a formatted version of the number. Every time the input'sinputevent 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
watchSince the
modelobtained fromdefineModelis 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, themodelis not exclusively mutated externally but also internally as a result of either internal or external changes.modelupdated as a result of internal changesWhen
onExternalUpdateis called due to a localmodelupdate, there are two possibilities:onExternalUpdaterun (i.e., do nothing). The execution ofonExternalUpdatedue to localmodelupdates might not produce adverse side effects. However, unnecesary operations would always be performed every timemodelis internally updated (see example 1).onExternalUpdateoperations, aninternalModelrefmight be implemented to handle internal changes. WheninternalModelis updated,modelshould be updated accordingly. Then, inonExternalUpdate, ifmodel.value === internalModel.valueit 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.modelupdated as a result of external changesSometimes,
modelmight be further updated insideonExternalUpdate. This will rerun the callback. Again, there are two possibilities:onExternalUpdaterun (i.e., do nothing). If the re-execution ofonExternalUpdateupdatesmodelwith its current value no further adverse side effects are produced. Again, the problem is performing unnecesary operations before themodelis re-updated (see example 2).modelvalue 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,
onExternalUpdatewill 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-modelseems 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
defineExposeAs 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
modelValueandupdate:modelValueprops (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
useModelcomposable 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(modelValueprop)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-modelcould be used to send the object in amodelValueprop; theupdate:modelValueprop should be ignored.An additional composable analogous to
defineModelmay be implemented to properly consume themodelValueprop.The suggested API is presented below.
See full example.
Beta Was this translation helpful? Give feedback.
All reactions