From e42cb543947d4286115b6adae6e8a5741d909f14 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 3 Apr 2020 21:37:58 -0400 Subject: [PATCH] fix(runtime-core): support attr merging on child with root level comments fix #904 --- .../rendererAttrsFallthrough.spec.ts | 34 ++++++++++- .../runtime-core/src/componentRenderUtils.ts | 60 +++++++++++++++---- 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts index 4719f15d..4f9d208f 100644 --- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts +++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts @@ -9,7 +9,8 @@ import { defineComponent, openBlock, createBlock, - FunctionalComponent + FunctionalComponent, + createCommentVNode } from '@vue/runtime-dom' import { mockWarn } from '@vue/shared' @@ -495,4 +496,35 @@ describe('attribute fallthrough', () => { expect(onClick).toHaveBeenCalledTimes(1) expect(onClick).toHaveBeenCalledWith('custom') }) + + it('should support fallthrough for fragments with single element + comments', () => { + const click = jest.fn() + + const Hello = { + setup() { + return () => h(Child, { class: 'foo', onClick: click }) + } + } + + const Child = { + setup(props: any) { + return () => [ + createCommentVNode('hello'), + h('button'), + createCommentVNode('world') + ] + } + } + + const root = document.createElement('div') + document.body.appendChild(root) + render(h(Hello), root) + + expect(root.innerHTML).toBe( + `` + ) + const button = root.children[0] as HTMLElement + button.dispatchEvent(new CustomEvent('click')) + expect(click).toHaveBeenCalled() + }) }) diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 6ba67280..8fbcf9c3 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -8,7 +8,10 @@ import { normalizeVNode, createVNode, Comment, - cloneVNode + cloneVNode, + Fragment, + VNodeArrayChildren, + isVNode } from './vnode' import { handleError, ErrorCodes } from './errorHandling' import { PatchFlags, ShapeFlags, EMPTY_OBJ, isOn } from '@vue/shared' @@ -80,22 +83,30 @@ export function renderComponentRoot( } // attr merging + // in dev mode, comments are preserved, and it's possible for a template + // to have comments along side the root element which makes it a fragment + let root = result + let setRoot: ((root: VNode) => void) | undefined = undefined + if (__DEV__) { + ;[root, setRoot] = getChildRoot(result) + } + if ( Component.inheritAttrs !== false && fallthroughAttrs && fallthroughAttrs !== EMPTY_OBJ ) { if ( - result.shapeFlag & ShapeFlags.ELEMENT || - result.shapeFlag & ShapeFlags.COMPONENT + root.shapeFlag & ShapeFlags.ELEMENT || + root.shapeFlag & ShapeFlags.COMPONENT ) { - result = cloneVNode(result, fallthroughAttrs) + root = cloneVNode(root, fallthroughAttrs) // If the child root node is a compiler optimized vnode, make sure it // force update full props to account for the merged attrs. - if (result.dynamicChildren) { - result.patchFlag |= PatchFlags.FULL_PROPS + if (root.dynamicChildren) { + root.patchFlag |= PatchFlags.FULL_PROPS } - } else if (__DEV__ && !accessedAttrs && result.type !== Comment) { + } else if (__DEV__ && !accessedAttrs && root.type !== Comment) { warn( `Extraneous non-props attributes (` + `${Object.keys(attrs).join(', ')}) ` + @@ -108,27 +119,33 @@ export function renderComponentRoot( // inherit scopeId const parentScopeId = parent && parent.type.__scopeId if (parentScopeId) { - result = cloneVNode(result, { [parentScopeId]: '' }) + root = cloneVNode(root, { [parentScopeId]: '' }) } // inherit directives if (vnode.dirs) { - if (__DEV__ && !isElementRoot(result)) { + if (__DEV__ && !isElementRoot(root)) { warn( `Runtime directive used on component with non-element root node. ` + `The directives will not function as intended.` ) } - result.dirs = vnode.dirs + root.dirs = vnode.dirs } // inherit transition data if (vnode.transition) { - if (__DEV__ && !isElementRoot(result)) { + if (__DEV__ && !isElementRoot(root)) { warn( `Component inside renders non-element root node ` + `that cannot be animated.` ) } - result.transition = vnode.transition + root.transition = vnode.transition + } + + if (__DEV__ && setRoot) { + setRoot(root) + } else { + result = root } } catch (err) { handleError(err, instance, ErrorCodes.RENDER_FUNCTION) @@ -139,6 +156,25 @@ export function renderComponentRoot( return result } +const getChildRoot = ( + vnode: VNode +): [VNode, ((root: VNode) => void) | undefined] => { + if (vnode.type !== Fragment) { + return [vnode, undefined] + } + const rawChildren = vnode.children as VNodeArrayChildren + const children = rawChildren.filter(child => { + return !(isVNode(child) && child.type === Comment) + }) + if (children.length !== 1) { + return [vnode, undefined] + } + const childRoot = children[0] + const index = rawChildren.indexOf(childRoot) + const setRoot = (updatedRoot: VNode) => (rawChildren[index] = updatedRoot) + return [normalizeVNode(childRoot), setRoot] +} + const getFallthroughAttrs = (attrs: Data): Data | undefined => { let res: Data | undefined for (const key in attrs) {