diff --git a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
index ef5630ff65a..79e2867ad69 100644
--- a/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
+++ b/packages/runtime-core/__tests__/rendererAttrsFallthrough.spec.ts
@@ -8,6 +8,8 @@ import {
type FunctionalComponent,
createBlock,
createCommentVNode,
+ createElementBlock,
+ createElementVNode,
defineComponent,
h,
mergeProps,
@@ -673,6 +675,58 @@ describe('attribute fallthrough', () => {
expect(click).toHaveBeenCalled()
})
+ it('should support fallthrough for nested dev root fragments', async () => {
+ const toggle = ref(false)
+
+ const Child = {
+ setup() {
+ return () => (
+ openBlock(),
+ createElementBlock(
+ Fragment,
+ null,
+ [
+ createCommentVNode(' comment A '),
+ toggle.value
+ ? (openBlock(), createElementBlock('span', { key: 0 }, 'Foo'))
+ : (openBlock(),
+ createElementBlock(
+ Fragment,
+ { key: 1 },
+ [
+ createCommentVNode(' comment B '),
+ createElementVNode('div', null, 'Bar'),
+ ],
+ PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
+ )),
+ ],
+ PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
+ )
+ )
+ },
+ }
+
+ const Root = {
+ setup() {
+ return () => (openBlock(), createBlock(Child, { class: 'red' }))
+ },
+ }
+
+ const root = document.createElement('div')
+ document.body.appendChild(root)
+ render(h(Root), root)
+
+ expect(root.innerHTML).toBe(
+ `
Bar
`,
+ )
+
+ toggle.value = true
+ await nextTick()
+ expect(root.innerHTML).toBe(
+ `Foo`,
+ )
+ })
+
// #1989
it('should not fallthrough v-model listeners with corresponding declared prop', () => {
let textFoo = ''
diff --git a/packages/runtime-core/__tests__/scopeId.spec.ts b/packages/runtime-core/__tests__/scopeId.spec.ts
index fb705cfad09..08753e023a1 100644
--- a/packages/runtime-core/__tests__/scopeId.spec.ts
+++ b/packages/runtime-core/__tests__/scopeId.spec.ts
@@ -1,14 +1,23 @@
import {
+ Fragment,
+ createBlock,
+ createCommentVNode,
+ createVNode,
+ defineComponent,
h,
+ nextTick,
nodeOps,
+ openBlock,
popScopeId,
pushScopeId,
+ ref,
render,
renderSlot,
serializeInner,
withScopeId,
} from '@vue/runtime-test'
import { withCtx } from '../src/componentRenderContext'
+import { PatchFlags } from '@vue/shared'
describe('scopeId runtime support', () => {
test('should attach scopeId', () => {
@@ -184,6 +193,55 @@ describe('scopeId runtime support', () => {
expect(serializeInner(root)).toBe(``)
})
+
+ test('should inherit scopeId through nested DEV_ROOT_FRAGMENT with inheritAttrs: false', async () => {
+ const Parent = {
+ __scopeId: 'parent',
+ render() {
+ return h(Child, { class: 'foo' })
+ },
+ }
+
+ const ok = ref(true)
+ const Child = defineComponent({
+ inheritAttrs: false,
+ render() {
+ return (
+ openBlock(),
+ createBlock(
+ Fragment,
+ null,
+ [
+ createCommentVNode('comment1'),
+ ok.value
+ ? (openBlock(), createBlock('div', { key: 0 }, 'div1'))
+ : (openBlock(),
+ createBlock(
+ Fragment,
+ { key: 1 },
+ [
+ createCommentVNode('comment2'),
+ createVNode('div', null, 'div2'),
+ ],
+ PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
+ )),
+ ],
+ PatchFlags.STABLE_FRAGMENT | PatchFlags.DEV_ROOT_FRAGMENT,
+ )
+ )
+ },
+ })
+
+ const root = nodeOps.createElement('div')
+ render(h(Parent), root)
+ expect(serializeInner(root)).toBe(`div1
`)
+
+ ok.value = false
+ await nextTick()
+ expect(serializeInner(root)).toBe(
+ `div2
`,
+ )
+ })
})
describe('backwards compat with <=3.0.7', () => {
diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts
index 4cb29180e25..4b83c699031 100644
--- a/packages/runtime-core/src/componentRenderUtils.ts
+++ b/packages/runtime-core/src/componentRenderUtils.ts
@@ -266,10 +266,17 @@ export function renderComponentRoot(
const getChildRoot = (vnode: VNode): [VNode, SetRootFn] => {
const rawChildren = vnode.children as VNodeArrayChildren
const dynamicChildren = vnode.dynamicChildren
- const childRoot = filterSingleRoot(rawChildren)
+ const childRoot = filterSingleRoot(rawChildren, false)
if (!childRoot) {
return [vnode, undefined]
+ } else if (
+ __DEV__ &&
+ childRoot.patchFlag > 0 &&
+ childRoot.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
+ ) {
+ return getChildRoot(childRoot)
}
+
const index = rawChildren.indexOf(childRoot)
const dynamicIndex = dynamicChildren ? dynamicChildren.indexOf(childRoot) : -1
const setRoot: SetRootFn = (updatedRoot: VNode) => {
@@ -287,6 +294,7 @@ const getChildRoot = (vnode: VNode): [VNode, SetRootFn] => {
export function filterSingleRoot(
children: VNodeArrayChildren,
+ recurse = true,
): VNode | undefined {
let singleRoot
for (let i = 0; i < children.length; i++) {
@@ -299,6 +307,14 @@ export function filterSingleRoot(
return
} else {
singleRoot = child
+ if (
+ __DEV__ &&
+ recurse &&
+ singleRoot.patchFlag > 0 &&
+ singleRoot.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT
+ ) {
+ return filterSingleRoot(singleRoot.children as VNodeArrayChildren)
+ }
}
}
} else {