button.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
})
+
+ // #1989
+ it('should not fallthrough v-model listeners with corresponding declared prop', () => {
+ let textFoo = ''
+ let textBar = ''
+ const click = jest.fn()
+
+ const App = defineComponent({
+ setup() {
+ return () =>
+ h(Child, {
+ modelValue: textFoo,
+ 'onUpdate:modelValue': (val: string) => {
+ textFoo = val
+ }
+ })
+ }
+ })
+
+ const Child = defineComponent({
+ props: ['modelValue'],
+ setup(_props, { emit }) {
+ return () =>
+ h(GrandChild, {
+ modelValue: textBar,
+ 'onUpdate:modelValue': (val: string) => {
+ textBar = val
+ emit('update:modelValue', 'from Child')
+ }
+ })
+ }
+ })
+
+ const GrandChild = defineComponent({
+ props: ['modelValue'],
+ setup(_props, { emit }) {
+ return () =>
+ h('button', {
+ onClick() {
+ click()
+ emit('update:modelValue', 'from GrandChild')
+ }
+ })
+ }
+ })
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(App), root)
+
+ const node = root.children[0] as HTMLElement
+
+ node.dispatchEvent(new CustomEvent('click'))
+ expect(click).toHaveBeenCalled()
+ expect(textBar).toBe('from GrandChild')
+ expect(textFoo).toBe('from Child')
+ })
})
import { PatchFlags, ShapeFlags, isOn, isModelListener } from '@vue/shared'
import { warn } from './warning'
import { isHmrUpdating } from './hmr'
+import { NormalizedProps } from './componentProps'
// mark the current rendering instance for asset resolution (e.g.
// resolveComponent, resolveDirective) during render
proxy,
withProxy,
props,
+ propsOptions: [propsOptions],
slots,
attrs,
emit,
shapeFlag & ShapeFlags.ELEMENT ||
shapeFlag & ShapeFlags.COMPONENT
) {
- if (shapeFlag & ShapeFlags.ELEMENT && keys.some(isModelListener)) {
- // #1643, #1543
- // component v-model listeners should only fallthrough for component
- // HOCs
- fallthroughAttrs = filterModelListeners(fallthroughAttrs)
+ if (propsOptions && keys.some(isModelListener)) {
+ // If a v-model listener (onUpdate:xxx) has a corresponding declared
+ // prop, it indicates this component expects to handle v-model and
+ // it should not fallthrough.
+ // related: #1543, #1643, #1989
+ fallthroughAttrs = filterModelListeners(
+ fallthroughAttrs,
+ propsOptions
+ )
}
root = cloneVNode(root, fallthroughAttrs)
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
return res
}
-const filterModelListeners = (attrs: Data): Data => {
+const filterModelListeners = (attrs: Data, props: NormalizedProps): Data => {
const res: Data = {}
for (const key in attrs) {
- if (!isModelListener(key)) {
+ if (!isModelListener(key) || !(key.slice(9) in props)) {
res[key] = attrs[key]
}
}