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

Two-way <input v-model> binding loses downstream sync in child component #10217

Closed
shengslogar opened this issue Jan 25, 2024 · 3 comments
Closed

Comments

@shengslogar
Copy link

shengslogar commented Jan 25, 2024

Vue version

3.4.15

Link to minimal reproduction

https://play.vuejs.org/#eNqNVF1v2jAU/St3eQlIJNHa7YVR1LWrtE5bN7VsfclLSC7ExbEt20lAiP++a4dSoB8qDyS5n+f6nOt18FWpuKkxGAYjk2umLBi0tRqnglVKagtr0DgbQJvZvIQNzLSsIKSMcBdxWTJeXMpKbZ1xsrO40uGXVKQil8JYqMwczlzBXvgdOZdwLzUvPoR9F5IkQDkVCguytmBLZoAzgWAlgaJHK6M2W4FZiRx8eCv1AqaY1ZbNas5X0DJbwlTSHxOqtiYVHneP+g6g14ezsYMQNxmvkYCErvEo6QankenDYqV4ZpG+AEblx/GEut5T11EuCxw3UUUPPkr8F1wwUTAxh1tUWo4SCndpXerJ+CazrEGYSBX9xAY5XDtQFHbSVXenDPS7WirMLRY0Spk1TOoYJiuFUKJGyETRTV9mFoSEDnvLOAeF2jBjY18s6aqN/OCwxXmWBjRvGkByAMzz4w5bSeGO+xVcfwW+G1m2D4yGFbbOHCNbjNAzSBIoYIErY7VcYP8Qdu4gRblT0UvYH5F/w2k9f0LqWdhndL32Gttstgw5evcoDQaBNYRjxubxg5GCZL/2BxO4zoyj/q0sI6mmwRC8x/loENn+8Daraxw82vMS88UL9gezdLY0+KPRoG4wDXY+m+k52s59dXeDS3rfOWnumlP0G85bNJKT3qXowi5qURDsvTiP9tqvJklzYq6WFoV5HMoBdZEbH58GtKBOB6+N/gT3ND71eanY0CkebPgLl8d23R2P/7bUFDijZf7lTL3wyRMOYG1JU0O4s5oAb/p0X7y1k88EviuVBk4qx4R3t1RUZeqI8s5xOC9N051raa0ywyTJC0Fp1IE1OhZoE6Gq5JzCEl0LyyqMClmdn8af4o+fk4KUvm+P0VTRVMuWVEBV9khyBbw0dKRRFLROROH7+h6lHfQ+8j3rv2Nw8x8zAQb5

Steps to reproduce

  1. Create a parent Vue component with a ref, a watcher that mutates that ref every time it changes, and a native HTML <input> bound to that ref with v-model
  2. Add a child Vue component <input> bound to that same ref through a v-model at the child and parent level
  3. Type in child input and watch child input value lose sync with ref

Sample code:

ParentComp.vue

<script setup>
import { ref, watch } from 'vue'
import ChildComp from './ChildComp.vue';

const msg = ref('Hello World!')
watch(msg, () => msg.value = '')
</script>

<template>
  <input v-model="msg" />
  <ChildComp v-model="msg" />
</template>

ChildComp.vue

<script setup>
const modelValue = defineModel('modelValue', {type: String});
</script>

<template>
  <input v-model="modelValue"/>
</template>

What is expected?

Child input behaves the same as parent input. It keeps sync with ref and its value is always "" after keyboard input.

What is actually happening?

Child input breaks sync with ref after multiple keyboard strokes.

System Info

No response

Any additional comments?

This issue seems fairly elementary, so I apologize if this has already been addressed. I did the obligatory Googles and searched through GitHub issues/discussions and came up empty-handed, but admittedly the search terms for this issue are a bit generic.

What seems to be happening, which I could imagine is a known limitation of the underlying sync mechanism, is values only get propagated down to child components when the primitive value changes. However, I don't feel like I'm doing anything unprecedented, so perhaps a note in the v-model docs would be warranted.

A use case here is an input mask, where something like +15555555555 gets formatted to (555) 555-5555 as a user types. This works fine under normal circumstances, as each keystroke corresponds with a formatted change, but once invalid characters are entered (e.g. no letters are allowed), the second invalid character that is entered will "break through" the v-model binding.

An obvious workaround is reaching into the DOM and manually setting an input's value, but I would like to avoid that if at all possible.

@dadaguai-git
Copy link

If you want to access the owner component's DOM in a watcher callback after Vue has updated it, you need to specify the flush: post
demo_20240126174341

@soyamore
Copy link

soyamore commented Jan 26, 2024

You can define msg as prop in the Child component and emit the value like the example below :

<script setup>
defineProps('msg', {type: String});
defineEmits(['update:value'])
</script>

<template>
  <input :value="msg" @input="$emit('update:value', $event.target.value) />
</template>

For the parent component

<script setup>
import { ref, watch } from 'vue'
import ChildComp from './ChildComp.vue';

const msg = ref('Hello World!')

const updateMsgValue = (newVal) => {
  msg.value = newVal
}
</script>

<template>
  <input v-model="msg" />
  <ChildComp :msg="msg" @update:value="updateMsgValue" />
</template>

@shengslogar
Copy link
Author

Thanks, @dadaguai-git, you nailed it! I had no idea that option existed. (I should probably read through the docs again.)

I'm not sure why this works, but my guess is a regular watcher is somehow shortcutting the normal event lifecycle, probably between the child @update:model-value event and the value that's getting passed back down, although I'm still not clear as to why this is needed with a child component, but not inside the parent component.

(Additionally, if you inspect the value present at the child component in the repro linked above, the correct value is being propagated downwards, but not being set at the native <input> level. That strikes me as odd.)

nextTick appears to behave identically to { flush: 'post' }. In both cases, msg.value gets updated twice for each keystroke, as expected:

<script setup>
import { ref, nextTick } from 'vue'
import ChildComp from './ChildComp.vue';

const msg = ref('Hello World!')

async function handleUpdate() {
     await nextTick();
     msg.value = ''
}
</script>

<template>
  <ChildComp v-model="msg" @update:model-value="handleUpdate" />
</template>

Closing this out as seemingly intended behavior, or at least one with a built-in workaround, but any further clarification would be appreciated.

@github-actions github-actions bot locked and limited conversation to collaborators Feb 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants