Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update v-model reference synchronously #8652

Closed
liamdebeasi opened this issue Jun 26, 2023 · 4 comments
Closed

update v-model reference synchronously #8652

liamdebeasi opened this issue Jun 26, 2023 · 4 comments
Labels
✨ feature request New feature or request

Comments

@liamdebeasi
Copy link

liamdebeasi commented Jun 26, 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:

el._assign = getModelAssigner(vnode)
const castToNumber =
number || (vnode.props && vnode.props.type === 'number')
addEventListener(el, lazy ? 'change' : 'input', e => {
if ((e.target as any).composing) return
let domValue: string | number = el.value
if (trim) {
domValue = domValue.trim()
}
if (castToNumber) {
domValue = looseToNumber(domValue)
}
el._assign(domValue)
and ). This does not work for the scenario provided above because the native <input /> is rendered outside of the Vue context.

vuejs/vue#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.

@JessicaSachs
Copy link
Contributor

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.

@jacekkarczmarczyk
Copy link
Contributor

@liamdebeasi
Copy link
Author

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 #8652 (comment) 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=

@liamdebeasi
Copy link
Author

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:

hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)

And then the internal event listener is added after here in the onVnodeBeforeMount hook:

invokeVNodeHook(vnodeHook, parentComponent, vnode)

Using the directive fixes the issue because the "created" hook is fired before the application event listener is added:

invokeDirectiveHook(vnode, null, parentComponent, 'created')

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==

@liamdebeasi liamdebeasi closed this as not planned Won't fix, can't repro, duplicate, stale Oct 27, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Nov 11, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
✨ feature request New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants