Skip to content

Commit e42cb54

Browse files
committed
fix(runtime-core): support attr merging on child with root level comments
fix #904
1 parent 5bf7251 commit e42cb54

File tree

2 files changed

+81
-13
lines changed

2 files changed

+81
-13
lines changed

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

+33-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
defineComponent,
1010
openBlock,
1111
createBlock,
12-
FunctionalComponent
12+
FunctionalComponent,
13+
createCommentVNode
1314
} from '@vue/runtime-dom'
1415
import { mockWarn } from '@vue/shared'
1516

@@ -495,4 +496,35 @@ describe('attribute fallthrough', () => {
495496
expect(onClick).toHaveBeenCalledTimes(1)
496497
expect(onClick).toHaveBeenCalledWith('custom')
497498
})
499+
500+
it('should support fallthrough for fragments with single element + comments', () => {
501+
const click = jest.fn()
502+
503+
const Hello = {
504+
setup() {
505+
return () => h(Child, { class: 'foo', onClick: click })
506+
}
507+
}
508+
509+
const Child = {
510+
setup(props: any) {
511+
return () => [
512+
createCommentVNode('hello'),
513+
h('button'),
514+
createCommentVNode('world')
515+
]
516+
}
517+
}
518+
519+
const root = document.createElement('div')
520+
document.body.appendChild(root)
521+
render(h(Hello), root)
522+
523+
expect(root.innerHTML).toBe(
524+
`<!--hello--><button class="foo"></button><!--world-->`
525+
)
526+
const button = root.children[0] as HTMLElement
527+
button.dispatchEvent(new CustomEvent('click'))
528+
expect(click).toHaveBeenCalled()
529+
})
498530
})

packages/runtime-core/src/componentRenderUtils.ts

+48-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
normalizeVNode,
99
createVNode,
1010
Comment,
11-
cloneVNode
11+
cloneVNode,
12+
Fragment,
13+
VNodeArrayChildren,
14+
isVNode
1215
} from './vnode'
1316
import { handleError, ErrorCodes } from './errorHandling'
1417
import { PatchFlags, ShapeFlags, EMPTY_OBJ, isOn } from '@vue/shared'
@@ -80,22 +83,30 @@ export function renderComponentRoot(
8083
}
8184

8285
// attr merging
86+
// in dev mode, comments are preserved, and it's possible for a template
87+
// to have comments along side the root element which makes it a fragment
88+
let root = result
89+
let setRoot: ((root: VNode) => void) | undefined = undefined
90+
if (__DEV__) {
91+
;[root, setRoot] = getChildRoot(result)
92+
}
93+
8394
if (
8495
Component.inheritAttrs !== false &&
8596
fallthroughAttrs &&
8697
fallthroughAttrs !== EMPTY_OBJ
8798
) {
8899
if (
89-
result.shapeFlag & ShapeFlags.ELEMENT ||
90-
result.shapeFlag & ShapeFlags.COMPONENT
100+
root.shapeFlag & ShapeFlags.ELEMENT ||
101+
root.shapeFlag & ShapeFlags.COMPONENT
91102
) {
92-
result = cloneVNode(result, fallthroughAttrs)
103+
root = cloneVNode(root, fallthroughAttrs)
93104
// If the child root node is a compiler optimized vnode, make sure it
94105
// force update full props to account for the merged attrs.
95-
if (result.dynamicChildren) {
96-
result.patchFlag |= PatchFlags.FULL_PROPS
106+
if (root.dynamicChildren) {
107+
root.patchFlag |= PatchFlags.FULL_PROPS
97108
}
98-
} else if (__DEV__ && !accessedAttrs && result.type !== Comment) {
109+
} else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
99110
warn(
100111
`Extraneous non-props attributes (` +
101112
`${Object.keys(attrs).join(', ')}) ` +
@@ -108,27 +119,33 @@ export function renderComponentRoot(
108119
// inherit scopeId
109120
const parentScopeId = parent && parent.type.__scopeId
110121
if (parentScopeId) {
111-
result = cloneVNode(result, { [parentScopeId]: '' })
122+
root = cloneVNode(root, { [parentScopeId]: '' })
112123
}
113124
// inherit directives
114125
if (vnode.dirs) {
115-
if (__DEV__ && !isElementRoot(result)) {
126+
if (__DEV__ && !isElementRoot(root)) {
116127
warn(
117128
`Runtime directive used on component with non-element root node. ` +
118129
`The directives will not function as intended.`
119130
)
120131
}
121-
result.dirs = vnode.dirs
132+
root.dirs = vnode.dirs
122133
}
123134
// inherit transition data
124135
if (vnode.transition) {
125-
if (__DEV__ && !isElementRoot(result)) {
136+
if (__DEV__ && !isElementRoot(root)) {
126137
warn(
127138
`Component inside <Transition> renders non-element root node ` +
128139
`that cannot be animated.`
129140
)
130141
}
131-
result.transition = vnode.transition
142+
root.transition = vnode.transition
143+
}
144+
145+
if (__DEV__ && setRoot) {
146+
setRoot(root)
147+
} else {
148+
result = root
132149
}
133150
} catch (err) {
134151
handleError(err, instance, ErrorCodes.RENDER_FUNCTION)
@@ -139,6 +156,25 @@ export function renderComponentRoot(
139156
return result
140157
}
141158

159+
const getChildRoot = (
160+
vnode: VNode
161+
): [VNode, ((root: VNode) => void) | undefined] => {
162+
if (vnode.type !== Fragment) {
163+
return [vnode, undefined]
164+
}
165+
const rawChildren = vnode.children as VNodeArrayChildren
166+
const children = rawChildren.filter(child => {
167+
return !(isVNode(child) && child.type === Comment)
168+
})
169+
if (children.length !== 1) {
170+
return [vnode, undefined]
171+
}
172+
const childRoot = children[0]
173+
const index = rawChildren.indexOf(childRoot)
174+
const setRoot = (updatedRoot: VNode) => (rawChildren[index] = updatedRoot)
175+
return [normalizeVNode(childRoot), setRoot]
176+
}
177+
142178
const getFallthroughAttrs = (attrs: Data): Data | undefined => {
143179
let res: Data | undefined
144180
for (const key in attrs) {

0 commit comments

Comments
 (0)