defineComponent,
openBlock,
createBlock,
- FunctionalComponent
+ FunctionalComponent,
+ createCommentVNode
} from '@vue/runtime-dom'
import { mockWarn } from '@vue/shared'
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(
+ `<!--hello--><button class="foo"></button><!--world-->`
+ )
+ const button = root.children[0] as HTMLElement
+ button.dispatchEvent(new CustomEvent('click'))
+ expect(click).toHaveBeenCalled()
+ })
})
normalizeVNode,
createVNode,
Comment,
- cloneVNode
+ cloneVNode,
+ Fragment,
+ VNodeArrayChildren,
+ isVNode
} from './vnode'
import { handleError, ErrorCodes } from './errorHandling'
import { PatchFlags, ShapeFlags, EMPTY_OBJ, isOn } from '@vue/shared'
}
// 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(', ')}) ` +
// 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 <Transition> 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)
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) {