From ca7dfd6cd76e0cd103d18b1a0193e032cfeca441 Mon Sep 17 00:00:00 2001 From: HcySunYang Date: Sun, 30 Aug 2020 15:17:53 +0800 Subject: [PATCH 1/3] fix(runtime-core): v-model listeners that already exists on the component should not be merged --- .../rendererAttrsFallthrough.spec.ts | 59 ++++++++++++++++++- .../runtime-core/src/componentRenderUtils.ts | 23 +++++--- 2 files changed, 74 insertions(+), 8 deletions(-) diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts index e2031eb66bb..938623b3a49 100644 --- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts @@ -11,7 +11,8 @@ import { createBlock, FunctionalComponent, createCommentVNode, - Fragment + Fragment, + SetupContext } from '@vue/runtime-dom' describe('attribute fallthrough', () => { @@ -277,6 +278,62 @@ describe('attribute fallthrough', () => { expect(node.hasAttribute('foo')).toBe(false) }) + it('should not fallthrough the v-model listeners that already exists on the component', async () => { + let textFoo = '' + let textBar = '' + const click = jest.fn() + + const App = { + setup() { + return () => + h(Child, { + modelValue: textFoo, + 'onUpdate:modelValue': (val: string) => { + textFoo = val + } + }) + } + } + + const Child = { + props: ['modelValue'], + setup(props: any, { emit }: SetupContext) { + return () => + h(GrandChild, { + modelValue: textBar, + 'onUpdate:modelValue': (val: string) => { + textBar = val + emit('update:modelValue', 'from Child') + } + }) + } + } + + const GrandChild = { + props: ['modelValue'], + setup(props: any, { emit }: SetupContext) { + 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') + }) + it('should not fallthrough with inheritAttrs: false', () => { const Parent = { render() { diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 9c7ff21a67a..390cd33f411 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -125,11 +125,17 @@ export function renderComponentRoot( 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 (keys.some(isModelListener)) { + fallthroughAttrs = + shapeFlag & ShapeFlags.ELEMENT + ? // #1643, #1543 + // component v-model listeners should only fallthrough for component + // HOCs + filterModelListeners(fallthroughAttrs) + : // #1989 + // if a component already has v-model listeners, + // then it should not merge the v-model listeners that fallthrough by the parent component. + filterModelListeners(fallthroughAttrs, root.props) } root = cloneVNode(root, fallthroughAttrs) } else if (__DEV__ && !accessedAttrs && root.type !== Comment) { @@ -251,10 +257,13 @@ const getFunctionalFallthrough = (attrs: Data): Data | undefined => { return res } -const filterModelListeners = (attrs: Data): Data => { +const filterModelListeners = ( + attrs: Data, + exsitingAttrs: Data | null = null +): Data => { const res: Data = {} for (const key in attrs) { - if (!isModelListener(key)) { + if (!isModelListener(key) || (exsitingAttrs && !exsitingAttrs[key])) { res[key] = attrs[key] } } From d74651a675aab6e73c5cfcad7ad4cbc2a1802a15 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 1 Sep 2020 22:18:01 -0400 Subject: [PATCH 2/3] refactor: adjust model listener fallthrough heuristics --- .../rendererAttrsFallthrough.spec.ts | 116 +++++++++--------- .../runtime-core/src/componentRenderUtils.ts | 29 ++--- 2 files changed, 71 insertions(+), 74 deletions(-) diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts index 938623b3a49..07348016fed 100644 --- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts @@ -11,8 +11,7 @@ import { createBlock, FunctionalComponent, createCommentVNode, - Fragment, - SetupContext + Fragment } from '@vue/runtime-dom' describe('attribute fallthrough', () => { @@ -278,62 +277,6 @@ describe('attribute fallthrough', () => { expect(node.hasAttribute('foo')).toBe(false) }) - it('should not fallthrough the v-model listeners that already exists on the component', async () => { - let textFoo = '' - let textBar = '' - const click = jest.fn() - - const App = { - setup() { - return () => - h(Child, { - modelValue: textFoo, - 'onUpdate:modelValue': (val: string) => { - textFoo = val - } - }) - } - } - - const Child = { - props: ['modelValue'], - setup(props: any, { emit }: SetupContext) { - return () => - h(GrandChild, { - modelValue: textBar, - 'onUpdate:modelValue': (val: string) => { - textBar = val - emit('update:modelValue', 'from Child') - } - }) - } - } - - const GrandChild = { - props: ['modelValue'], - setup(props: any, { emit }: SetupContext) { - 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') - }) - it('should not fallthrough with inheritAttrs: false', () => { const Parent = { render() { @@ -651,4 +594,61 @@ describe('attribute fallthrough', () => { 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') + }) }) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 390cd33f411..d6d05e1dc74 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -17,6 +17,7 @@ import { handleError, ErrorCodes } from './errorHandling' 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 @@ -46,6 +47,7 @@ export function renderComponentRoot( proxy, withProxy, props, + propsOptions: [propsOptions], slots, attrs, emit, @@ -125,17 +127,15 @@ export function renderComponentRoot( shapeFlag & ShapeFlags.ELEMENT || shapeFlag & ShapeFlags.COMPONENT ) { - if (keys.some(isModelListener)) { - fallthroughAttrs = - shapeFlag & ShapeFlags.ELEMENT - ? // #1643, #1543 - // component v-model listeners should only fallthrough for component - // HOCs - filterModelListeners(fallthroughAttrs) - : // #1989 - // if a component already has v-model listeners, - // then it should not merge the v-model listeners that fallthrough by the parent component. - filterModelListeners(fallthroughAttrs, root.props) + if (propsOptions && keys.some(isModelListener)) { + // If a v-model listener (onUpdate:xxx) has a corresponding decalred + // 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) { @@ -257,13 +257,10 @@ const getFunctionalFallthrough = (attrs: Data): Data | undefined => { return res } -const filterModelListeners = ( - attrs: Data, - exsitingAttrs: Data | null = null -): Data => { +const filterModelListeners = (attrs: Data, props: NormalizedProps): Data => { const res: Data = {} for (const key in attrs) { - if (!isModelListener(key) || (exsitingAttrs && !exsitingAttrs[key])) { + if (!isModelListener(key) || !(key.slice(9) in props)) { res[key] = attrs[key] } } From e015b067afd8b0f05f845884eedc05524e02f29b Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 1 Sep 2020 22:20:02 -0400 Subject: [PATCH 3/3] chore: typo [ci skip] --- packages/runtime-core/src/componentRenderUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index d6d05e1dc74..9fa72c9dd51 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -128,7 +128,7 @@ export function renderComponentRoot( shapeFlag & ShapeFlags.COMPONENT ) { if (propsOptions && keys.some(isModelListener)) { - // If a v-model listener (onUpdate:xxx) has a corresponding decalred + // 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