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 {