update v-model reference synchronously
What problem does this feature solve?
Vue components with v-model
that wrap Web Components have their v-model
reference updated a few frames after the associated event fires. This can be confusing as logging the model value in the event callback shows a stale value.
Example
This example shows an AppInput
Vue component that renders an app-input
Web Component.
The application sets the v-model
of AppInput
using the inputValue
ref. The value of this model is passed to the app-input
Web Component where the value is then set on the <input />
element that it renders. Whenever the user types in the text input, inputValue
should be updated with the value of the text input.
The model is updated in AppInput.ts
by calling emit('update:modelValue', ev.target.value)
whenever the Web Component fires the custominput
CustomEvent. While this works, it leads to a confusing developer experience. The reason for this is developers can add their own custominput
listener by adding @custominput
on the AppInput
Vue component. Logging the value of inputValue
logs a stale value because emit
updates the model asynchronously.
Steps to reproduce the issue:
- Open code reproduction.
- Type something into the text field. Observe that the logged model value is outdated. Wrapping this
console.log
in arequestAnimationFrame
should log the latest value.
Other Information
Vue has access to the model internally and is able to update it synchronously if Vue is the one rendering the <input />
(see: https://github.com/vuejs/core/blob/020851e57d9a9f727c6ea07e9c1575430af02b73/packages/runtime-dom/src/directives/vModel.ts#L47-L59 and https://github.com/vuejs/core/blob/020851e57d9a9f727c6ea07e9c1575430af02b73/packages/compiler-core/src/transforms/vModel.ts#L83). This does not work for the scenario provided above because the native <input />
is rendered outside of the Vue context.
https://github.com/vuejs/vue/issues/7830 appears to be related.
What does the proposed API look like?
Ideally I'd be able to do something like emit('update:modelValue', ev.target.value, { sync: true })
. I don't need access to the model ref itself.
The alternative approach is to use two events which is what I am doing now, but it is hard to maintain and error prone. For example, the app-input
Web Component would fire the v-custominput
event. The AppInput
Vue component would listen for v-custominput
, update the model using emit
, and then call emit('custominput')
so developer callbacks that listen for custominput
are fired after the model has been updated.
Was the sync modifier removed in Vue 3? I can't find docs about it - I'm looking for the raw render fn version of it.
If I understand the migration guide correctly, it sounds like I could be doing v-model:value
and then emit update:value
. Unfortunately, that does not address the root problem in https://github.com/vuejs/core/issues/8652#issue-1775279546 where the reference is updated a frame after the custominput
callback fires.
I am going to close this as I no longer need this feature. I used the "created" hook in a custom directive instead of the "onVnodeBeforeMount" VNode hook. The problem was that the application event listener and the internal event listener were added to the same element, but the application event listener was added first. This caused the reactive reference to be updated too late since the internal event listener fired after the application event listener.
The application event listener is added first here: https://github.com/vuejs/core/blob/edf2572615d0b065bb7ae49de4c3b71086771310/packages/runtime-core/src/renderer.ts#L660-L670
And then the internal event listener is added after here in the onVnodeBeforeMount
hook: https://github.com/vuejs/core/blob/edf2572615d0b065bb7ae49de4c3b71086771310/packages/runtime-core/src/renderer.ts#L686
Using the directive fixes the issue because the "created" hook is fired before the application event listener is added: https://github.com/vuejs/core/blob/edf2572615d0b065bb7ae49de4c3b71086771310/packages/runtime-core/src/renderer.ts#L652