Subscribe on changes!

update v-model reference synchronously

avatar
Jun 26th 2023

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

Link: https://play.vuejs.org/#eNqdVk1v2zgQ/SuEsIDkwJYO3ZNrB9lms9gu2m7RBslFh8rS2FYrkVySchIY/u87HOqDkt2iqA+GRM7Xe5x51DH4Q8r40ECwDFY6V6U0TINp5HXKy1oKZdiRoclbLhvDTmyrRM3COOmWwteenYJtb4Ih7V7Kc8G1YaU1fsiqBtja2kVhOPO2Bb9ttBG1S7NmERxmbH3NjilnzJqICuJK7KLwE+agOOHcCxof7L+NeLJBV4mDgiDwxUAtq8wAvjG26sEcFrUooFqnwRAnDdhNTpXQGu6NKot+gwNwM0uD61VPAYZdJV6OYB44RhZ1JuOvWnDkloBgJtrQabB00OwaMmXf02BvjNTLJMkLjm5YWnlQMQeTcFknN2iWqIabsoZFIeqbV/Gr+PekKLXxl2PQ9WKjxJMGhUHSYO6lSXDxAGqhgBegQP1s2ombn3qydZbeZscjOSEpRuNBbsvdhJJc1LKsQP0rTYkHPaImqyrx9A+tGdVAjyXfQ/7twvpX/ewwfVRAlXn4TaZ2YNz23ecP8IzP/SZ2QlO1x/CdzU+ATdjYGp3Zm4YXWLZnR9W+pRMu+e5e3z0b4LoDZQslNsiezuP2B9CHcpFtj8Wu62KjkcN+9HQlzF9VttP3WHs3hGngjm+fKSjSwB/VArYlpwIEx46es/3gRA05GJsXCejx8AGn5dwIJ7jKtO414vGWYQXYDpr9ff/+3V0FNcbvcI1kAAmkKgoKY7dJDFSTG6GiWedjf7qRgEtoaN9oxu0DMsRo8sfWCgVMcWb2pY6HjFNnlLnWGbXG8564YaGHvsC+SIcE98j6vwbUy2eogCoPaa/Vt86r3LKI1ke5ek6cgHXxzopuCx/V72ZujLz15qAs+Rjuy8oVag8R1cxQZ7Pk+ssvQxqXnRXFnZXEdygGgGk7yznDwjoBH34uUW6BcnhiTlopQBR6uovuR7ZpNpsKtJscdvJzD/lRhGRm8r2LkdMV0BM26pe+wzhCguIWp3WT5d8u8NcRO3hTAHimcXAQ+ltkPR2lKJJKSG0RQF3iLPo8JAn7k8wxE7BH2LDez3YIF4ZllYKseGnDFu3cYPc4ftqB0jH2fhRmUi7ao2HrtTdSI1QTT2fiO8+98T0bsgrnhG7Kj4irG4nQXe6E6eqqTXXFHvfArT4PR4ndti1RjfGKP8fc+z0B9gMUzAjWyALvUbJtb2j7vYCXC88h7hwSTy8w8gNHuzewFQreC7wM7RfEwa4tnWxNW5H2YqgutO+4CyOMQAaXmvmMlAhiJ9ssc+pHlLa8z9yIT7rYtkgUOsxLCth93IyjX+7r12wQA5K8s6Fr1/fj0z5e4GzuxHA5heVPET4Hp/8BxkJXEg==

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:

  1. Open code reproduction.
  2. Type something into the text field. Observe that the logged model value is outdated. Wrapping this console.log in a requestAnimationFrame 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.

avatar
Jul 12th 2023

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.

avatar
Jul 12th 2023

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.

Here is a new SFC playground with the changes I made: https://play.vuejs.org/#eNqdVk1v2zgQ/SuEsIDkwJYO3ZNrB9lms9gu2m7RBslFh8rS2FYrkVySchIY/u87HOqDkt2iqA+GRM7Xe5x51DH4Q8r40ECwDFY6V6U0TINp5HXKy1oKZdiRoclbLhvDTmyrRM3COOmWwteenYJtb4Ih7V7Kc8G1YaU1fsiqBtja2kVhOPO2Bb9ttBG1S7NmERxmbH3NjilnzJqICuJK7KLwE+agOOHcCxof7L+NeLJBV4mDgiDwxUAtq8wAvjG26sEcFrUooFqS6zoNhmhpwG5yqofWcG9UX/QbHICbWRpcr3oiMPgq8TIF88DxsqgzGX/VgiPDBAcz0YZOg6UDaNeQL/ueBntjpF4mSV5wdMMCy4OKOZiEyzq5QbNENdyUNSwKUd+8il/FvydFqY2/HIOuFxslnjQoDJIGcy9NgosHUAsFvAAF6mfTTtz81JOts/Q2Ox7MCUkxGo9zW+4mlOSilmUF6l9pSjzuETVZVYmnf2jNqAZ6LPke8m8X1r/qZ4fpowKqzMNvMrUD47bvPn+AZ3zuN7Efmqo9hu9sfgJsxcbW6MzeNLzAsj07qvYtnXDJd/f67tkA1x0oWyixQfZ0Hrc/gD6Ui2x7LHZdFxuNHPYDqCth/qqynb7H2rtRTAN3fPtMQZEG/sAWsC05FSA4dvSc7QcnasjB2LxIQI+HDzgz50Y4x1Wmda8Uj7cMK8B20Ozv+/fv7iqoMX6HayQGSCBVUVAYu02SoJrcCBXNOh/7040EXEJD+0aTbh+QIUZDPLZWKGOKM7MvdTxknDqj2LXOqDie98QNCz30BfZFOiS4R9b/NaBePkMFVHlIe63KdV7llkW0PsrVc+JkrIt3VnRb+Kh+N3Nj5K03B2XJx3BfVq5Qe4ioZoY6myXXX34Z0rjsrCjurCS+QzEATNtZzhkW1sn48HOJcguUwxNz0koBotDTXXQ/sk2z2VSg3eSwk597yI8iJDOT712MnC6CnrBRv/QdxhESFLc4rZss/3aBv47YwZsCwDONg4PQ3yXr6ShFkVRCaosA6hJn0echSdifZI6ZgD3ChvV+tkO4MCyrFGTFSxu2aOcGu8fx0w6UjrH3ozCTctEeDVuvvZEaoZp4OhPfee6N79mQVTgndF9+RFzdSITuiidMV1dtqiv2uAdu9Xk4Suy2bYlqjBf9Oebe7wmwH6BgRrBGFniPkm17T9uvBrxceA5x55B4eoGRHzjavYGtUPBe4GVovyMOdm3pZGvairQXQ3WhfcddGGEEMrjUzGekRBA72WaZUz+itOV95kZ80sW2RaLQYXbfI5h1HPhyS79mgw6Q2p3NW7u+Hx/08QJdc6eDyykif4DwOTj9D3k7V3g=

avatar
Oct 27th 2023

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

Demo: https://play.vuejs.org/#eNqdVl2P0zgU/StX0UpJR23ywD6VdjS7w6yWFQsIRswDQSJNbttAYhvbaWdU9b9zbSepkxaE6EOV2PfrHN97nEPwlxDxrsFgHixULkuhQaFuxHXKylpwqeEAZPKSiUbDEdaS1xDGSbcUPvfsJK57Ewpp9lKWc6Y0lMb4Q1Y1CEtjF4XhxNvm7LZRmtcuzRIi3E1geQ2HlAEYE15hXPFNFL6jHDZOOPWCxjvzbyIeTdBF4qAQCHrRWIsq00hvAIsezG5W8wKrZRqc4qQB3OS2ErtGe4PKoj9wh0xP0uB60VNAYReJlyOYBo6RWZ2J+IvijLi1QCiT3VBpMHfQzBoxZd7TYKu1UPMkyQtGblRauZMxQ50wUSc3ZJbIhumyxlnB65tn8bP4z6QolfaXY1T1bCX5XqGkIGkw9dIktLhDOZPICpQofzXtyM1PPdo6S2+y05EciRSt6CDX5WZESc5rUVYo3whd0kEPqMmqiu//s2taNthjybeYf72w/kU9OkxvJdrKPPw6kxvUbvvu/Wt8pOd+kzqhqdpj+MHmO6QmbEyNzuzvhhVUtmdnq31pT7hkm3t196iRqQ6UKdSyYe3tedz+BPqpXGLbY7Hrulgr4rAfPVVx/U+VbdQ91d4NYRq449tmEos08Ee1wHXJbAGcUUdPYTuFfam3L0qJuS53qE5BbIOenPWTQIrw4TVNz7kRTXSVKdVrxsMtUEXUHgr+vf//1V2FNeXrcA5kgQi1VRU2jNm24iCbXHMZTTof81ONQFoiQ/NmZ948EGNglWBoLUnQJAO9LVV8yjh2JtlrnUl7PO+RGxW66wvsi3RIaM9af2tQPr3HiqikykO71+pd51WuIbLrg1w9J07QunhnRbeFD+p3MzhE3nozlIZ8Cvd54Qo1h0jqpm2nQ3L9+bchDcvOiuLOSOQrEgektJ3lFKiwTtBPP5coN0AZ7sFJrQ0QhZ4Ok/sBVs1qVaFykwRHP/cpP4mSyHS+dTFyeyX0hA36pe8wRpCwuKXpXWX51wv8dcSevG0AfLTj4CD0t8pyPFpRJCQXyiDAuqTZ9HlIEnhhzSkTwgOuoPczHcK4hqySmBVPbdiinRvqHsdPO1Aqpt6PwkyIWXs0sFx6IzVANfJ0Jr7z1BvfsyGraE7szfmWcHUjEbrL3mK6umpTXcHDFpnR69NRUretSWMUXfnnmHu/PVI/YAGaQyMKuletbXtjm+8HumxYjnHnkHh60cLrlYyq67HnxCUd9pw+MKpxP2J1oXuHTRjhHKzBpV4+4yTC2Kk4ZE78LKMt7RM34aMmNh0ShQ7y3AbsvnWG0S+29SVJsMJ3Nnrt+lDwo+2wBQ5ODedjYMfJ9OPHEcefPvnTMXkeHL8DPnRjqQ==