fix(runtime-core): support attr merging on child with root level comments

fix #904
This commit is contained in:
Evan You 2020-04-03 21:37:58 -04:00
parent 5bf72517ce
commit e42cb54394
2 changed files with 81 additions and 13 deletions

View File

@ -9,7 +9,8 @@ import {
defineComponent, defineComponent,
openBlock, openBlock,
createBlock, createBlock,
FunctionalComponent FunctionalComponent,
createCommentVNode
} from '@vue/runtime-dom' } from '@vue/runtime-dom'
import { mockWarn } from '@vue/shared' import { mockWarn } from '@vue/shared'
@ -495,4 +496,35 @@ describe('attribute fallthrough', () => {
expect(onClick).toHaveBeenCalledTimes(1) expect(onClick).toHaveBeenCalledTimes(1)
expect(onClick).toHaveBeenCalledWith('custom') 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(
`<!--hello--><button class="foo"></button><!--world-->`
)
const button = root.children[0] as HTMLElement
button.dispatchEvent(new CustomEvent('click'))
expect(click).toHaveBeenCalled()
})
}) })

View File

@ -8,7 +8,10 @@ import {
normalizeVNode, normalizeVNode,
createVNode, createVNode,
Comment, Comment,
cloneVNode cloneVNode,
Fragment,
VNodeArrayChildren,
isVNode
} from './vnode' } from './vnode'
import { handleError, ErrorCodes } from './errorHandling' import { handleError, ErrorCodes } from './errorHandling'
import { PatchFlags, ShapeFlags, EMPTY_OBJ, isOn } from '@vue/shared' import { PatchFlags, ShapeFlags, EMPTY_OBJ, isOn } from '@vue/shared'
@ -80,22 +83,30 @@ export function renderComponentRoot(
} }
// attr merging // 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 ( if (
Component.inheritAttrs !== false && Component.inheritAttrs !== false &&
fallthroughAttrs && fallthroughAttrs &&
fallthroughAttrs !== EMPTY_OBJ fallthroughAttrs !== EMPTY_OBJ
) { ) {
if ( if (
result.shapeFlag & ShapeFlags.ELEMENT || root.shapeFlag & ShapeFlags.ELEMENT ||
result.shapeFlag & ShapeFlags.COMPONENT 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 // If the child root node is a compiler optimized vnode, make sure it
// force update full props to account for the merged attrs. // force update full props to account for the merged attrs.
if (result.dynamicChildren) { if (root.dynamicChildren) {
result.patchFlag |= PatchFlags.FULL_PROPS root.patchFlag |= PatchFlags.FULL_PROPS
} }
} else if (__DEV__ && !accessedAttrs && result.type !== Comment) { } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
warn( warn(
`Extraneous non-props attributes (` + `Extraneous non-props attributes (` +
`${Object.keys(attrs).join(', ')}) ` + `${Object.keys(attrs).join(', ')}) ` +
@ -108,27 +119,33 @@ export function renderComponentRoot(
// inherit scopeId // inherit scopeId
const parentScopeId = parent && parent.type.__scopeId const parentScopeId = parent && parent.type.__scopeId
if (parentScopeId) { if (parentScopeId) {
result = cloneVNode(result, { [parentScopeId]: '' }) root = cloneVNode(root, { [parentScopeId]: '' })
} }
// inherit directives // inherit directives
if (vnode.dirs) { if (vnode.dirs) {
if (__DEV__ && !isElementRoot(result)) { if (__DEV__ && !isElementRoot(root)) {
warn( warn(
`Runtime directive used on component with non-element root node. ` + `Runtime directive used on component with non-element root node. ` +
`The directives will not function as intended.` `The directives will not function as intended.`
) )
} }
result.dirs = vnode.dirs root.dirs = vnode.dirs
} }
// inherit transition data // inherit transition data
if (vnode.transition) { if (vnode.transition) {
if (__DEV__ && !isElementRoot(result)) { if (__DEV__ && !isElementRoot(root)) {
warn( warn(
`Component inside <Transition> renders non-element root node ` + `Component inside <Transition> renders non-element root node ` +
`that cannot be animated.` `that cannot be animated.`
) )
} }
result.transition = vnode.transition root.transition = vnode.transition
}
if (__DEV__ && setRoot) {
setRoot(root)
} else {
result = root
} }
} catch (err) { } catch (err) {
handleError(err, instance, ErrorCodes.RENDER_FUNCTION) handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
@ -139,6 +156,25 @@ export function renderComponentRoot(
return result 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 => { const getFallthroughAttrs = (attrs: Data): Data | undefined => {
let res: Data | undefined let res: Data | undefined
for (const key in attrs) { for (const key in attrs) {