Skip to content

Commit 63f1f18

Browse files
authored
fix(runtime-core): v-model listeners that already exists on the component should not be merged (#2011)
fix #1989
1 parent aa757e8 commit 63f1f18

File tree

2 files changed

+70
-7
lines changed

2 files changed

+70
-7
lines changed

packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts

+57
Original file line numberDiff line numberDiff line change
@@ -594,4 +594,61 @@ describe('attribute fallthrough', () => {
594594
button.dispatchEvent(new CustomEvent('click'))
595595
expect(click).toHaveBeenCalled()
596596
})
597+
598+
// #1989
599+
it('should not fallthrough v-model listeners with corresponding declared prop', () => {
600+
let textFoo = ''
601+
let textBar = ''
602+
const click = jest.fn()
603+
604+
const App = defineComponent({
605+
setup() {
606+
return () =>
607+
h(Child, {
608+
modelValue: textFoo,
609+
'onUpdate:modelValue': (val: string) => {
610+
textFoo = val
611+
}
612+
})
613+
}
614+
})
615+
616+
const Child = defineComponent({
617+
props: ['modelValue'],
618+
setup(_props, { emit }) {
619+
return () =>
620+
h(GrandChild, {
621+
modelValue: textBar,
622+
'onUpdate:modelValue': (val: string) => {
623+
textBar = val
624+
emit('update:modelValue', 'from Child')
625+
}
626+
})
627+
}
628+
})
629+
630+
const GrandChild = defineComponent({
631+
props: ['modelValue'],
632+
setup(_props, { emit }) {
633+
return () =>
634+
h('button', {
635+
onClick() {
636+
click()
637+
emit('update:modelValue', 'from GrandChild')
638+
}
639+
})
640+
}
641+
})
642+
643+
const root = document.createElement('div')
644+
document.body.appendChild(root)
645+
render(h(App), root)
646+
647+
const node = root.children[0] as HTMLElement
648+
649+
node.dispatchEvent(new CustomEvent('click'))
650+
expect(click).toHaveBeenCalled()
651+
expect(textBar).toBe('from GrandChild')
652+
expect(textFoo).toBe('from Child')
653+
})
597654
})

packages/runtime-core/src/componentRenderUtils.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { handleError, ErrorCodes } from './errorHandling'
1717
import { PatchFlags, ShapeFlags, isOn, isModelListener } from '@vue/shared'
1818
import { warn } from './warning'
1919
import { isHmrUpdating } from './hmr'
20+
import { NormalizedProps } from './componentProps'
2021

2122
// mark the current rendering instance for asset resolution (e.g.
2223
// resolveComponent, resolveDirective) during render
@@ -46,6 +47,7 @@ export function renderComponentRoot(
4647
proxy,
4748
withProxy,
4849
props,
50+
propsOptions: [propsOptions],
4951
slots,
5052
attrs,
5153
emit,
@@ -125,11 +127,15 @@ export function renderComponentRoot(
125127
shapeFlag & ShapeFlags.ELEMENT ||
126128
shapeFlag & ShapeFlags.COMPONENT
127129
) {
128-
if (shapeFlag & ShapeFlags.ELEMENT && keys.some(isModelListener)) {
129-
// #1643, #1543
130-
// component v-model listeners should only fallthrough for component
131-
// HOCs
132-
fallthroughAttrs = filterModelListeners(fallthroughAttrs)
130+
if (propsOptions && keys.some(isModelListener)) {
131+
// If a v-model listener (onUpdate:xxx) has a corresponding declared
132+
// prop, it indicates this component expects to handle v-model and
133+
// it should not fallthrough.
134+
// related: #1543, #1643, #1989
135+
fallthroughAttrs = filterModelListeners(
136+
fallthroughAttrs,
137+
propsOptions
138+
)
133139
}
134140
root = cloneVNode(root, fallthroughAttrs)
135141
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
@@ -251,10 +257,10 @@ const getFunctionalFallthrough = (attrs: Data): Data | undefined => {
251257
return res
252258
}
253259

254-
const filterModelListeners = (attrs: Data): Data => {
260+
const filterModelListeners = (attrs: Data, props: NormalizedProps): Data => {
255261
const res: Data = {}
256262
for (const key in attrs) {
257-
if (!isModelListener(key)) {
263+
if (!isModelListener(key) || !(key.slice(9) in props)) {
258264
res[key] = attrs[key]
259265
}
260266
}

0 commit comments

Comments
 (0)