From dde70761200653e86c27e5cb0e41d54eda99bd02 Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 29 May 2025 17:04:49 +0800 Subject: [PATCH 01/16] feat(compiler-vapor): add support for forwarded slots --- packages/compiler-vapor/src/generate.ts | 7 +++++ .../src/generators/slotOutlet.ts | 6 +++-- packages/compiler-vapor/src/ir/index.ts | 2 ++ packages/compiler-vapor/src/transform.ts | 1 + .../src/transforms/transformSlotOutlet.ts | 27 +++++++++++++++++++ packages/runtime-vapor/src/componentSlots.ts | 16 ++++++++++- packages/runtime-vapor/src/index.ts | 2 +- 7 files changed, 57 insertions(+), 4 deletions(-) diff --git a/packages/compiler-vapor/src/generate.ts b/packages/compiler-vapor/src/generate.ts index 193a0f5da77..ff3806611ad 100644 --- a/packages/compiler-vapor/src/generate.ts +++ b/packages/compiler-vapor/src/generate.ts @@ -18,6 +18,7 @@ import { genCall, } from './generators/utils' import { setTemplateRefIdent } from './generators/templateRef' +import { createForwardedSlotIdent } from './generators/slotOutlet' export type CodegenOptions = Omit @@ -129,6 +130,12 @@ export function generate( `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`, ) } + if (ir.hasForwardedSlot) { + push( + NEWLINE, + `const ${createForwardedSlotIdent} = ${context.helper('forwardedSlotCreator')}()`, + ) + } push(...genBlockContent(ir.block, context, true)) push(INDENT_END, NEWLINE) diff --git a/packages/compiler-vapor/src/generators/slotOutlet.ts b/packages/compiler-vapor/src/generators/slotOutlet.ts index 3221cbbd2c7..dc992ae2334 100644 --- a/packages/compiler-vapor/src/generators/slotOutlet.ts +++ b/packages/compiler-vapor/src/generators/slotOutlet.ts @@ -5,12 +5,14 @@ import { genExpression } from './expression' import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils' import { genRawProps } from './component' +export const createForwardedSlotIdent = `_createForwardedSlot` + export function genSlotOutlet( oper: SlotOutletIRNode, context: CodegenContext, ): CodeFragment[] { const { helper } = context - const { id, name, fallback } = oper + const { id, name, fallback, forwarded } = oper const [frag, push] = buildCodeFragment() const nameExpr = name.isStatic @@ -26,7 +28,7 @@ export function genSlotOutlet( NEWLINE, `const n${id} = `, ...genCall( - helper('createSlot'), + forwarded ? createForwardedSlotIdent : helper('createSlot'), nameExpr, genRawProps(oper.props, context) || 'null', fallbackArg, diff --git a/packages/compiler-vapor/src/ir/index.ts b/packages/compiler-vapor/src/ir/index.ts index da636113224..086f77ca612 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -66,6 +66,7 @@ export interface RootIRNode { directive: Set block: BlockIRNode hasTemplateRef: boolean + hasForwardedSlot: boolean } export interface IfIRNode extends BaseIRNode { @@ -209,6 +210,7 @@ export interface SlotOutletIRNode extends BaseIRNode { name: SimpleExpressionNode props: IRProps[] fallback?: BlockIRNode + forwarded?: boolean parent?: number anchor?: number } diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 76563899d2b..93488ae95a1 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -230,6 +230,7 @@ export function transform( directive: new Set(), block: newBlock(node), hasTemplateRef: false, + hasForwardedSlot: false, } const context = new TransformContext(ir, node, options) diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 83b4aa2d2e4..159d70c3814 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -5,6 +5,7 @@ import { ErrorCodes, NodeTypes, type SimpleExpressionNode, + type TemplateChildNode, createCompilerError, createSimpleExpression, isStaticArgOf, @@ -99,6 +100,13 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } return () => { + let forwarded = false + const slotNode = context.block.node + if (slotNode.type === NodeTypes.ELEMENT) { + forwarded = hasForwardedSlots(slotNode.children) + } + if (forwarded) context.ir.hasForwardedSlot = true + exitBlock && exitBlock() context.dynamic.operation = { type: IRNodeTypes.SLOT_OUTLET_NODE, @@ -106,6 +114,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { name: slotName, props: irProps, fallback, + forwarded, } } } @@ -131,3 +140,21 @@ function createFallback( context.reference() return [fallback, exitBlock] } + +// TODO +function hasForwardedSlots(children: TemplateChildNode[]): boolean { + for (let i = 0; i < children.length; i++) { + const child = children[i] + switch (child.type) { + case NodeTypes.ELEMENT: + if ( + child.tagType === ElementTypes.SLOT || + hasForwardedSlots(child.children) + ) { + return true + } + break + } + } + return false +} diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 74296e09466..00ae4ea29ac 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -87,10 +87,24 @@ export function getSlot( } } +export function forwardedSlotCreator(): ( + name: string | (() => string), + rawProps?: LooseRawProps | null, + fallback?: VaporSlot, +) => Block { + const instance = currentInstance as VaporComponentInstance + return ( + name: string | (() => string), + rawProps?: LooseRawProps | null, + fallback?: VaporSlot, + ) => createSlot(name, rawProps, fallback, instance) +} + export function createSlot( name: string | (() => string), rawProps?: LooseRawProps | null, fallback?: VaporSlot, + i?: VaporComponentInstance, ): Block { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor @@ -98,7 +112,7 @@ export function createSlot( locateHydrationNode() } - const instance = currentInstance as VaporComponentInstance + const instance = i || (currentInstance as VaporComponentInstance) const rawSlots = instance.rawSlots const slotProps = rawProps ? new Proxy(rawProps, rawPropsProxyHandlers) diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 682532fa4d8..10d0aa63384 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -9,7 +9,7 @@ export { insert, prepend, remove, isFragment, VaporFragment } from './block' export { setInsertionState } from './insertionState' export { createComponent, createComponentWithFallback } from './component' export { renderEffect } from './renderEffect' -export { createSlot } from './componentSlots' +export { createSlot, forwardedSlotCreator } from './componentSlots' export { template } from './dom/template' export { createTextNode, child, nthChild, next } from './dom/node' export { From a952b0335897e18f4bf2dc68c22bfeb8a8993d1f Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 29 May 2025 21:45:00 +0800 Subject: [PATCH 02/16] test: add tests --- .../__snapshots__/vSlot.spec.ts.snap | 91 +++++++++++++++++++ .../__tests__/transforms/vSlot.spec.ts | 29 ++++++ packages/compiler-vapor/src/transform.ts | 1 + .../src/transforms/transformSlotOutlet.ts | 14 +-- .../compiler-vapor/src/transforms/vSlot.ts | 9 +- .../__tests__/componentSlots.spec.ts | 54 ++++++++++- 6 files changed, 190 insertions(+), 8 deletions(-) diff --git a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap index 4ecd8c76a7e..d1d80d4d620 100644 --- a/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap +++ b/packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap @@ -103,6 +103,97 @@ export function render(_ctx) { }" `; +exports[`compiler: transform slot > forwarded slots > 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n1 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createForwardedSlot("default", null) + return n0 + } + }) + return n1 + } + }, true) + return n2 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag only 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n1 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createForwardedSlot("default", null) + return n0 + } + }, true) + return n1 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag w/ template 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n2 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createForwardedSlot("default", null) + return n0 + } + }, true) + return n2 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag w/ v-for 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createFor as _createFor, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n3 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createFor(() => (_ctx.b), (_for_item0) => { + const n2 = _createForwardedSlot("default", null) + return n2 + }) + return n0 + } + }, true) + return n3 +}" +`; + +exports[`compiler: transform slot > forwarded slots > tag w/ v-if 1`] = ` +"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createIf as _createIf, createComponentWithFallback as _createComponentWithFallback } from 'vue'; + +export function render(_ctx) { + const _createForwardedSlot = _forwardedSlotCreator() + const _component_Comp = _resolveComponent("Comp") + const n3 = _createComponentWithFallback(_component_Comp, null, { + "default": () => { + const n0 = _createIf(() => (_ctx.ok), () => { + const n2 = _createForwardedSlot("default", null) + return n2 + }) + return n0 + } + }, true) + return n3 +}" +`; + exports[`compiler: transform slot > implicit default slot 1`] = ` "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue'; const t0 = _template("
") diff --git a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts index 84ddb2e5d04..a7da3d542f7 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -409,6 +409,35 @@ describe('compiler: transform slot', () => { }) }) + describe('forwarded slots', () => { + test(' tag only', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + + test(' tag w/ v-if', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + + test(' tag w/ v-for', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + + test(' tag w/ template', () => { + const { code } = compileWithSlots( + ``, + ) + expect(code).toMatchSnapshot() + }) + + test('', () => { + const { code } = compileWithSlots(``) + expect(code).toMatchSnapshot() + }) + }) + describe('errors', () => { test('error on extraneous children w/ named default slot', () => { const onError = vi.fn() diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 93488ae95a1..6d07ebcaf52 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -76,6 +76,7 @@ export class TransformContext { inVOnce: boolean = false inVFor: number = 0 + inSlot: number = 0 comment: CommentNode[] = [] component: Set = this.ir.component diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index 159d70c3814..a281c90a70d 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -100,11 +100,14 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } return () => { - let forwarded = false - const slotNode = context.block.node - if (slotNode.type === NodeTypes.ELEMENT) { - forwarded = hasForwardedSlots(slotNode.children) - } + const { + block: { node: slotNode }, + inSlot, + } = context + const forwarded = + inSlot !== 0 && + slotNode.type === NodeTypes.ELEMENT && + hasForwardedSlots(slotNode.children) if (forwarded) context.ir.hasForwardedSlot = true exitBlock && exitBlock() @@ -141,7 +144,6 @@ function createFallback( return [fallback, exitBlock] } -// TODO function hasForwardedSlots(children: TemplateChildNode[]): boolean { for (let i = 0; i < children.length; i++) { const child = children[i] diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index d1bf1c6b05f..2e767cb41cd 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -237,7 +237,14 @@ function createSlotBlock( const block: SlotBlockIRNode = newBlock(slotNode) block.props = dir && dir.exp const exitBlock = context.enterBlock(block) - return [block, exitBlock] + context.inSlot++ + return [ + block, + () => { + context.inSlot-- + exitBlock() + }, + ] } function isNonWhitespaceContent(node: TemplateChildNode): boolean { diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 58076fff9ee..46bfc3d938d 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -7,6 +7,7 @@ import { createSlot, createVaporApp, defineVaporComponent, + forwardedSlotCreator, insert, prepend, renderEffect, @@ -15,7 +16,7 @@ import { import { currentInstance, nextTick, ref } from '@vue/runtime-dom' import { makeRender } from './_utils' import type { DynamicSlot } from '../src/componentSlots' -import { setElementText } from '../src/dom/prop' +import { setElementText, setText } from '../src/dom/prop' const define = makeRender() @@ -503,4 +504,55 @@ describe('component: slots', () => { expect(host.innerHTML).toBe('

') }) }) + + describe('forwarded slot', () => { + test('should work', async () => { + const Child = defineVaporComponent({ + setup() { + return createSlot('foo', null) + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + Child, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('bar') + }) + }) }) From c23d63582e3ddc569b84102b22757549b97ea8f0 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 30 May 2025 09:17:04 +0800 Subject: [PATCH 03/16] chore: update --- packages/compiler-vapor/src/transform.ts | 2 +- .../src/transforms/transformSlotOutlet.ts | 31 +---------- .../compiler-vapor/src/transforms/vSlot.ts | 4 +- .../__tests__/componentSlots.spec.ts | 51 +++++++++++++++++++ packages/runtime-vapor/src/componentSlots.ts | 7 +-- 5 files changed, 58 insertions(+), 37 deletions(-) diff --git a/packages/compiler-vapor/src/transform.ts b/packages/compiler-vapor/src/transform.ts index 6d07ebcaf52..763e9612cdc 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -76,7 +76,7 @@ export class TransformContext { inVOnce: boolean = false inVFor: number = 0 - inSlot: number = 0 + inSlot: boolean = false comment: CommentNode[] = [] component: Set = this.ir.component diff --git a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts index a281c90a70d..dc2b620ddb2 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -5,7 +5,6 @@ import { ErrorCodes, NodeTypes, type SimpleExpressionNode, - type TemplateChildNode, createCompilerError, createSimpleExpression, isStaticArgOf, @@ -100,16 +99,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } return () => { - const { - block: { node: slotNode }, - inSlot, - } = context - const forwarded = - inSlot !== 0 && - slotNode.type === NodeTypes.ELEMENT && - hasForwardedSlots(slotNode.children) - if (forwarded) context.ir.hasForwardedSlot = true - + if (context.inSlot) context.ir.hasForwardedSlot = true exitBlock && exitBlock() context.dynamic.operation = { type: IRNodeTypes.SLOT_OUTLET_NODE, @@ -117,7 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { name: slotName, props: irProps, fallback, - forwarded, + forwarded: context.inSlot, } } } @@ -143,20 +133,3 @@ function createFallback( context.reference() return [fallback, exitBlock] } - -function hasForwardedSlots(children: TemplateChildNode[]): boolean { - for (let i = 0; i < children.length; i++) { - const child = children[i] - switch (child.type) { - case NodeTypes.ELEMENT: - if ( - child.tagType === ElementTypes.SLOT || - hasForwardedSlots(child.children) - ) { - return true - } - break - } - } - return false -} diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 2e767cb41cd..525fa323d3a 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -237,11 +237,11 @@ function createSlotBlock( const block: SlotBlockIRNode = newBlock(slotNode) block.props = dir && dir.exp const exitBlock = context.enterBlock(block) - context.inSlot++ + context.inSlot = true return [ block, () => { - context.inSlot-- + context.inSlot = false exitBlock() }, ] diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 46bfc3d938d..bdbd960363d 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -554,5 +554,56 @@ describe('component: slots', () => { await nextTick() expect(host.innerHTML).toBe('bar') }) + + test('mixed with non-forwarded slot', async () => { + const Child = defineVaporComponent({ + setup() { + return [createSlot('foo', null)] + }, + }) + const Parent = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent(Child, null, { + foo: () => { + const n0 = createForwardedSlot('foo', null) + return n0 + }, + }) + const n3 = createSlot('default', null) + return [n2, n3] + }, + }) + + const foo = ref('foo') + const { host } = define({ + setup() { + const n2 = createComponent( + Parent, + null, + { + foo: () => { + const n0 = template(' ')() as any + renderEffect(() => setText(n0, foo.value)) + return n0 + }, + default: () => { + const n3 = template(' ')() as any + renderEffect(() => setText(n3, foo.value)) + return n3 + }, + }, + true, + ) + return n2 + }, + }).render() + + expect(host.innerHTML).toBe('foofoo') + + foo.value = 'bar' + await nextTick() + expect(host.innerHTML).toBe('barbar') + }) }) }) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 00ae4ea29ac..19e9b5b6d1a 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -93,11 +93,8 @@ export function forwardedSlotCreator(): ( fallback?: VaporSlot, ) => Block { const instance = currentInstance as VaporComponentInstance - return ( - name: string | (() => string), - rawProps?: LooseRawProps | null, - fallback?: VaporSlot, - ) => createSlot(name, rawProps, fallback, instance) + return (name, rawProps, fallback) => + createSlot(name, rawProps, fallback, instance) } export function createSlot( From dcf927ff8c1a7662771b30b7f76cbb63afa8ebe7 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 30 May 2025 16:27:02 +0800 Subject: [PATCH 04/16] fix(vdomInterop): handle forwarded vapor slots during render VDOM slot --- packages/runtime-vapor/src/componentProps.ts | 3 ++- packages/runtime-vapor/src/vdomInterop.ts | 28 ++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts index a5e9daad229..7a0e9ed9286 100644 --- a/packages/runtime-vapor/src/componentProps.ts +++ b/packages/runtime-vapor/src/componentProps.ts @@ -210,7 +210,8 @@ export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean { if (dynamicSources) { let i = dynamicSources.length while (i--) { - if (hasOwn(resolveSource(dynamicSources[i]), key)) { + const source = resolveSource(dynamicSources[i]) + if (source && hasOwn(source, key)) { return true } } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 77228fd72a0..e7c7e02e0bd 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -26,7 +26,14 @@ import { mountComponent, unmountComponent, } from './component' -import { type Block, VaporFragment, insert, remove } from './block' +import { + type Block, + VaporFragment, + insert, + isFragment, + isValidBlock, + remove, +} from './block' import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' @@ -230,7 +237,24 @@ function renderVDOMSlot( isFunction(name) ? name() : name, props, ) - if ((vnode.children as any[]).length) { + let isValidSlotContent + let children = vnode.children as any[] + + // TODO add tests + // handle forwarded vapor slot + let vaporSlot + if (children.length === 1 && (vaporSlot = children[0].vs)) { + const block = vaporSlot.slot(props) + isValidSlotContent = + isValidBlock(block) || + // if block is a vapor fragment with insert, it indicates a forwarded VDOM slot + (isFragment(block) && block.insert) + } + // vnode children + else { + isValidSlotContent = children.length > 0 + } + if (isValidSlotContent) { if (fallbackNodes) { remove(fallbackNodes, parentNode) fallbackNodes = undefined From 7cfec7fcfbc2801c785a445e1814e8e8319f3827 Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 11 Jun 2025 17:34:31 +0800 Subject: [PATCH 05/16] wip: vdom slot interop --- .../runtime-core/src/helpers/renderSlot.ts | 21 + packages/runtime-core/src/index.ts | 4 + packages/runtime-core/src/renderer.ts | 2 +- .../__tests__/componentSlots.spec.ts | 2127 ++++++++++++++++- packages/runtime-vapor/src/block.ts | 2 +- packages/runtime-vapor/src/componentSlots.ts | 39 +- packages/runtime-vapor/src/vdomInterop.ts | 112 +- 7 files changed, 2275 insertions(+), 32 deletions(-) diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index 152c5a4b81c..c17cb1e3105 100644 --- a/packages/runtime-core/src/helpers/renderSlot.ts +++ b/packages/runtime-core/src/helpers/renderSlot.ts @@ -81,6 +81,10 @@ export function renderSlot( } openBlock() const validSlotContent = slot && ensureValidVNode(slot(props)) + + // handle forwarded vapor slot fallback + ensureVaporSlotFallback(validSlotContent, fallback) + const slotKey = props.key || // slot content array of a dynamic conditional slot may have a branch @@ -124,3 +128,20 @@ export function ensureValidVNode( ? vnodes : null } + +export function ensureVaporSlotFallback( + vnodes: VNodeArrayChildren | null | undefined, + fallback?: () => VNodeArrayChildren, +): void { + let vaporSlot: any + if ( + vnodes && + vnodes.length === 1 && + isVNode(vnodes[0]) && + (vaporSlot = vnodes[0].vs) + ) { + if (!vaporSlot.fallback && fallback) { + vaporSlot.fallback = fallback + } + } +} diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index e309554f2f6..f921eb0a2cf 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -557,3 +557,7 @@ export { startMeasure, endMeasure } from './profiling' * @internal */ export { initFeatureFlags } from './featureFlags' +/** + * @internal + */ +export { ensureVaporSlotFallback } from './helpers/renderSlot' diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5a18d62a8e1..a1468816789 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -2622,7 +2622,7 @@ export function traverseStaticChildren( function locateNonHydratedAsyncRoot( instance: ComponentInternalInstance, ): ComponentInternalInstance | undefined { - const subComponent = instance.subTree.component + const subComponent = instance.vapor ? null : instance.subTree.component if (subComponent) { if (subComponent.asyncDep && !subComponent.asyncResolved) { return subComponent diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index bdbd960363d..642be2a139f 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -12,8 +12,17 @@ import { prepend, renderEffect, template, + vaporInteropPlugin, } from '../src' -import { currentInstance, nextTick, ref } from '@vue/runtime-dom' +import { + createApp, + createSlots, + currentInstance, + h, + nextTick, + ref, + renderSlot, +} from '@vue/runtime-dom' import { makeRender } from './_utils' import type { DynamicSlot } from '../src/componentSlots' import { setElementText, setText } from '../src/dom/prop' @@ -470,6 +479,43 @@ describe('component: slots', () => { expect(html()).toBe('content') }) + test('use fallback on initial render', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const toggle = ref(false) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return createIf( + () => toggle.value, + () => { + return document.createTextNode('content') + }, + ) + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + toggle.value = true + await nextTick() + expect(html()).toBe('content') + + toggle.value = false + await nextTick() + expect(html()).toBe('fallback') + }) + test('dynamic slot work with v-if', async () => { const val = ref('header') const toggle = ref(false) @@ -605,5 +651,2084 @@ describe('component: slots', () => { await nextTick() expect(host.innerHTML).toBe('barbar') }) + + describe('vdom interop', () => { + test('vdom slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporSlot, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VaporForwardedSlotWithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporSlot, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
forwarded fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlotWithFallback as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VaporForwardedSlotWithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
forwarded fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlotWithFallback as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VdomForwardedSlot = { + render(this: any) { + return h(VaporSlot as any, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VdomForwardedSlot = { + render(this: any) { + return h(VaporSlot as any, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlotWithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
forwarded fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlotWithFallback as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
forwarded fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VdomForwardedSlotWithFallback = { + render(this: any) { + return h(VaporSlot as any, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => { + return [h('div', 'vdom fallback')] + }), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlotWithFallback as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot(empty) > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => { + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VdomForwardedSlotWithFallback = { + render(this: any) { + return h(VaporSlot as any, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => { + return [h('div', 'vdom fallback')] + }), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlotWithFallback as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => h(VaporForwardedSlot as any) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('
vdom fallback
') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VdomForwardedSlot = { + render(this: any) { + return h(VdomSlot, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VdomForwardedSlot = { + render(this: any) { + return h(VdomSlot, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlotWithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlotWithFallback as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VdomForwardedSlotWithFallback = { + render(this: any) { + return h(VdomSlot, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlotWithFallback as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VdomForwardedSlot = { + render(this: any) { + return h(VdomSlot, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlot2 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot1, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VdomForwardedSlotWithFallback = { + render(this: any) { + return h(VdomSlot, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VaporForwardedSlot2 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomForwardedSlotWithFallback as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot1, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
vdom fallback
', + ) + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VdomForwardedSlot = { + render(this: any) { + return h(VaporSlot as any, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const App = { + setup() { + return () => + h( + VdomForwardedSlot, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VdomForwardedSlot = { + render(this: any) { + return h(VaporForwardedSlot as any, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const App = { + setup() { + return () => + h( + VdomForwardedSlot, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VdomForwardedSlot2 = { + render(this: any) { + return h(VaporForwardedSlot as any, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot1 = { + render(this: any) { + return h(VdomForwardedSlot2, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot = { + render(this: any) { + return h(VdomForwardedSlot1, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const App = { + setup() { + return () => + h( + VdomForwardedSlot, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VaporForwardedSlot = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const VdomForwardedSlot2 = { + render(this: any) { + return h(VaporForwardedSlot as any, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot1 = { + render(this: any) { + return h(VdomForwardedSlot2, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot = { + render(this: any) { + return h(VdomForwardedSlot1, null, { + foo: () => [renderSlot(this.$slots, 'foo')], + _: 3 /* FORWARDED */, + }) + }, + } + + const App = { + setup() { + return () => + h( + VdomForwardedSlot, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VaporForwardedSlot2 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot1 as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VaporForwardedSlot2 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1WithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor1 fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot1WithFallback as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VaporForwardedSlot2WithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor2 fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2WithFallback, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot1 as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor2 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VaporForwardedSlot2 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporSlot, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1 = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2, + null, + { + foo: () => { + return createForwardedSlot('foo', null) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot1 as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VaporForwardedSlot2WithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VdomSlot as any, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor2 fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1WithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2WithFallback, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor1 fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot1WithFallback as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vapor1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VaporForwardedSlot2WithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporSlot, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor2 fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const VaporForwardedSlot1WithFallback = defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + VaporForwardedSlot2WithFallback, + null, + { + foo: () => { + return createForwardedSlot('foo', null, () => { + const n2 = template('
vapor1 fallback
')() + return n2 + }) + }, + }, + true, + ) + return n2 + }, + }) + + const App = { + setup() { + return () => + h( + VaporForwardedSlot1WithFallback as any, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe( + '
vapor1 fallback
', + ) + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VdomForwardedSlot2WithFallback = { + render(this: any) { + return h(VaporSlot as any, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom2 fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot1WithFallback = { + render(this: any) { + return h(VdomForwardedSlot2WithFallback, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom1 fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const App = { + setup() { + return () => + h( + VdomForwardedSlot1WithFallback, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vdom slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VdomSlot = { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'fallback'), + ]) + }, + } + + const VdomForwardedSlot2WithFallback = { + render(this: any) { + return h(VdomSlot, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom2 fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot1WithFallback = { + render(this: any) { + return h(VdomForwardedSlot2WithFallback, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom1 fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const App = { + setup() { + return () => + h( + VdomForwardedSlot1WithFallback, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) (multiple) > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + const n2 = template('
fallback
')() + return n2 + }) + return n0 + }, + }) + + const VdomForwardedSlot3WithFallback = { + render(this: any) { + return h(VaporSlot as any, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom3 fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot2WithFallback = { + render(this: any) { + return h(VdomForwardedSlot3WithFallback, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom2 fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const VdomForwardedSlot1WithFallback = { + render(this: any) { + return h(VdomForwardedSlot2WithFallback, null, { + foo: () => [ + renderSlot(this.$slots, 'foo', {}, () => [ + h('div', 'vdom1 fallback'), + ]), + ], + _: 3 /* FORWARDED */, + }) + }, + } + + const App = { + setup() { + return () => + h( + VdomForwardedSlot1WithFallback, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') + + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') + + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
vdom1 fallback
') + + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + }) }) }) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b782afd38d3..b2d0ca04b67 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -23,6 +23,7 @@ export class VaporFragment { anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void + fallback?: BlockFn constructor(nodes: Block) { this.nodes = nodes @@ -33,7 +34,6 @@ export class DynamicFragment extends VaporFragment { anchor: Node scope: EffectScope | undefined current?: BlockFn - fallback?: BlockFn constructor(anchorLabel?: string) { super([]) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 19e9b5b6d1a..b8af4c91629 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,5 +1,12 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' -import { type Block, type BlockFn, DynamicFragment, insert } from './block' +import { + type Block, + type BlockFn, + DynamicFragment, + type VaporFragment, + insert, + isFragment, +} from './block' import { rawPropsProxyHandlers } from './componentProps' import { currentInstance, isRef } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' @@ -138,8 +145,27 @@ export function createSlot( (slot._bound = () => { const slotContent = slot(slotProps) if (slotContent instanceof DynamicFragment) { - slotContent.fallback = fallback + let nodes = slotContent.nodes + if ( + (slotContent.fallback = fallback) && + isArray(nodes) && + nodes.length === 0 + ) { + // use fallback if the slot content is invalid + slotContent.update(fallback) + } else { + while (isFragment(nodes)) { + ensureVaporSlotFallback(nodes, fallback) + nodes = nodes.nodes + } + } } + // forwarded vdom slot, if there is no fallback provide, try use the fallback + // provided by the slot outlet. + else if (isFragment(slotContent)) { + ensureVaporSlotFallback(slotContent, fallback) + } + return slotContent }), ) @@ -162,3 +188,12 @@ export function createSlot( return fragment } + +function ensureVaporSlotFallback( + block: VaporFragment, + fallback?: VaporSlot, +): void { + if (block.insert && !block.fallback && fallback) { + block.fallback = fallback + } +} diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index e7c7e02e0bd..a6b115f7474 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -4,7 +4,9 @@ import { type ConcreteComponent, MoveType, type Plugin, + type RendererElement, type RendererInternals, + type RendererNode, type ShallowRef, type Slots, type VNode, @@ -12,6 +14,8 @@ import { createVNode, currentInstance, ensureRenderer, + ensureVaporSlotFallback, + isVNode, onScopeDispose, renderSlot, shallowRef, @@ -28,13 +32,13 @@ import { } from './component' import { type Block, + DynamicFragment, VaporFragment, insert, isFragment, - isValidBlock, remove, } from './block' -import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' +import { EMPTY_OBJ, extend, isArray, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' import type { RawSlots, VaporSlot } from './componentSlots' import { renderEffect } from './renderEffect' @@ -106,7 +110,22 @@ const vaporInteropImpl: Omit< // TODO fallback for slot with v-if content // fallback is a vnode slot function here, and slotBlock, if a DynamicFragment, // expects a Vapor BlockFn as fallback - fallback + // fallback + + // forwarded vdom slot without its own fallback, use the fallback provided by + // the slot outlet + if (slotBlock instanceof DynamicFragment) { + // vapor slot's nodes is a forwarded vdom slot + let nodes = slotBlock.nodes + while (isFragment(nodes)) { + ensureVDOMSlotFallback(nodes, fallback) + nodes = nodes.nodes + } + } else if (isFragment(slotBlock)) { + ensureVDOMSlotFallback(slotBlock, fallback) + } + + // TODO use fragment's anchor as selfAnchor? insert((n2.vb = slotBlock), container, selfAnchor) } else { // update @@ -229,51 +248,56 @@ function renderVDOMSlot( let fallbackNodes: Block | undefined let oldVNode: VNode | null = null + frag.fallback = fallback frag.insert = (parentNode, anchor) => { if (!isMounted) { renderEffect(() => { - const vnode = renderSlot( - slotsRef.value, - isFunction(name) ? name() : name, - props, - ) - let isValidSlotContent - let children = vnode.children as any[] + let vnode: VNode | undefined + let isValidSlot = false + // only render slot if rawSlots is defined and slot nodes are not empty + // otherwise, render fallback + if (slotsRef.value) { + vnode = renderSlot( + slotsRef.value, + isFunction(name) ? name() : name, + props, + ) - // TODO add tests - // handle forwarded vapor slot - let vaporSlot - if (children.length === 1 && (vaporSlot = children[0].vs)) { - const block = vaporSlot.slot(props) - isValidSlotContent = - isValidBlock(block) || - // if block is a vapor fragment with insert, it indicates a forwarded VDOM slot - (isFragment(block) && block.insert) + let children = vnode.children as any[] + // handle forwarded vapor slot without its own fallback + // use the fallback provided by the slot outlet + ensureVaporSlotFallback(children, fallback as any) + isValidSlot = children.length > 0 } - // vnode children - else { - isValidSlotContent = children.length > 0 - } - if (isValidSlotContent) { + + if (isValidSlot) { if (fallbackNodes) { remove(fallbackNodes, parentNode) fallbackNodes = undefined } internals.p( oldVNode, - vnode, + vnode!, parentNode, anchor, parentComponent as any, ) - oldVNode = vnode + oldVNode = vnode! } else { + // for forwarded slot without its own fallback, use the fallback + // provided by the slot outlet. + // re-fetch `frag.fallback` as it may have been updated at `createSlot` + fallback = frag.fallback if (fallback && !fallbackNodes) { // mount fallback if (oldVNode) { internals.um(oldVNode, parentComponent as any, null, true) } - insert((fallbackNodes = fallback(props)), parentNode, anchor) + insert( + (fallbackNodes = fallback(internals, parentComponent)), + parentNode, + anchor, + ) } oldVNode = null } @@ -315,3 +339,37 @@ export const vaporInteropPlugin: Plugin = app => { return mount(...args) }) satisfies App['mount'] } + +function ensureVDOMSlotFallback(block: VaporFragment, fallback?: () => any) { + if (block.insert && !block.fallback && fallback) { + block.fallback = createFallback(fallback) + } +} + +const createFallback = + (fallback: () => any) => + ( + internals: RendererInternals, + parentComponent: ComponentInternalInstance | null, + ) => { + const fallbackNodes = fallback() + + // vnode slot, wrap it as a VaporFragment + if (isArray(fallbackNodes) && fallbackNodes.every(isVNode)) { + const frag = new VaporFragment([]) + frag.insert = (parentNode, anchor) => { + fallbackNodes.forEach(vnode => { + internals.p(null, vnode, parentNode, anchor, parentComponent) + }) + } + frag.remove = parentNode => { + fallbackNodes.forEach(vnode => { + internals.um(vnode, parentComponent, null, true) + }) + } + return frag + } + + // vapor slot + return fallbackNodes as Block + } From f4f038873fffdd80f487b73e2a3b38c86d1f6dfd Mon Sep 17 00:00:00 2001 From: daiwei Date: Thu, 12 Jun 2025 15:54:40 +0800 Subject: [PATCH 06/16] wip: refactor tests --- .../__tests__/componentSlots.spec.ts | 1811 +++-------------- 1 file changed, 299 insertions(+), 1512 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 642be2a139f..4af9ef74eec 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -15,6 +15,7 @@ import { vaporInteropPlugin, } from '../src' import { + type Ref, createApp, createSlots, currentInstance, @@ -653,29 +654,46 @@ describe('component: slots', () => { }) describe('vdom interop', () => { - test('vdom slot > vapor forwarded slot > vapor slot', async () => { - const foo = ref('foo') - const show = ref(true) - - const VaporSlot = defineVaporComponent({ + const createVaporSlot = (fallbackText = 'fallback') => { + return defineVaporComponent({ setup() { const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() + const n2 = template(`
${fallbackText}
`)() return n2 }) return n0 }, }) + } + + const createVdomSlot = (fallbackText = 'fallback') => { + return { + render(this: any) { + return renderSlot(this.$slots, 'foo', {}, () => [ + h('div', fallbackText), + ]) + }, + } + } - const VaporForwardedSlot = defineVaporComponent({ + const createVaporForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return defineVaporComponent({ setup() { const createForwardedSlot = forwardedSlotCreator() const n2 = createComponent( - VaporSlot, + targetComponent, null, { foo: () => { - return createForwardedSlot('foo', null) + return fallbackText + ? createForwardedSlot('foo', null, () => { + const n2 = template(`
${fallbackText}
`)() + return n2 + }) + : createForwardedSlot('foo', null) }, }, true, @@ -683,12 +701,60 @@ describe('component: slots', () => { return n2 }, }) + } + + const createVdomForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return { + render(this: any) { + return h(targetComponent, null, { + foo: () => [ + fallbackText + ? renderSlot(this.$slots, 'foo', {}, () => [ + h('div', fallbackText), + ]) + : renderSlot(this.$slots, 'foo'), + ], + _: 3 /* FORWARDED */, + }) + }, + } + } + + const createMultipleVaporForwardedSlots = ( + targetComponent: any, + count: number, + ) => { + let current = targetComponent + for (let i = 0; i < count; i++) { + current = createVaporForwardedSlot(current) + } + return current + } + + const createMultipleVdomForwardedSlots = ( + targetComponent: any, + count: number, + ) => { + let current = targetComponent + for (let i = 0; i < count; i++) { + current = createVdomForwardedSlot(current) + } + return current + } - const App = { + const createTestApp = ( + rootComponent: any, + foo: Ref, + show: Ref, + ) => { + return { setup() { return () => h( - VaporForwardedSlot as any, + rootComponent, null, createSlots({ _: 2 /* DYNAMIC */ } as any, [ show.value @@ -702,6 +768,23 @@ describe('component: slots', () => { ) }, } + } + + const createEmptyTestApp = (rootComponent: any) => { + return { + setup() { + return () => h(rootComponent) + }, + } + } + + test('vdom slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -720,54 +803,12 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VaporForwardedSlotWithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporSlot, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
forwarded fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlotWithFallback as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VaporSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -786,49 +827,9 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VdomSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -851,52 +852,12 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VaporForwardedSlotWithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
forwarded fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlotWithFallback as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -915,60 +876,10 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VdomForwardedSlot = { - render(this: any) { - return h(VaporSlot as any, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -991,63 +902,13 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VdomForwardedSlot = { - render(this: any) { - return h(VaporSlot as any, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlotWithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
forwarded fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlotWithFallback as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomForwardedSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1070,64 +931,15 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VdomForwardedSlotWithFallback = { - render(this: any) { - return h(VaporSlot as any, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => { - return [h('div', 'vdom fallback')] - }), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlotWithFallback as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1147,51 +959,15 @@ describe('component: slots', () => { }) test('vdom slot(empty) > vapor forwarded slot > vdom forwarded slot(with fallback) > vapor slot', async () => { - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VdomForwardedSlotWithFallback = { - render(this: any) { - return h(VaporSlot as any, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => { - return [h('div', 'vdom fallback')] - }), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlotWithFallback as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => h(VaporForwardedSlot as any) - }, - } + const VaporSlot = createVaporSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createEmptyTestApp(VaporForwardedSlot) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1202,58 +978,10 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VdomForwardedSlot = { - render(this: any) { - return h(VdomSlot, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1276,61 +1004,13 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VdomForwardedSlot = { - render(this: any) { - return h(VdomSlot, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlotWithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlotWithFallback as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomForwardedSlot, + 'vapor fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1353,62 +1033,16 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VdomForwardedSlotWithFallback = { - render(this: any) { - return h(VdomSlot, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlotWithFallback as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) + const VdomSlot = createVdomSlot() - const App = { - setup() { - return () => - h( - VaporForwardedSlot as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createVaporForwardedSlot( + VdomForwardedSlotWithFallback, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1431,104 +1065,25 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } + const VdomSlot = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlot, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) - const VdomForwardedSlot = { - render(this: any) { - return h(VdomSlot, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } + const root = document.createElement('div') + createApp(App).use(vaporInteropPlugin).mount(root) + expect(root.innerHTML).toBe('foo') - const VaporForwardedSlot2 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) + foo.value = 'bar' + await nextTick() + expect(root.innerHTML).toBe('bar') - const VaporForwardedSlot1 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot1, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } - - const root = document.createElement('div') - createApp(App).use(vaporInteropPlugin).mount(root) - expect(root.innerHTML).toBe('foo') - - foo.value = 'bar' - await nextTick() - expect(root.innerHTML).toBe('bar') - - show.value = false - await nextTick() - expect(root.innerHTML).toBe('
fallback
') + show.value = false + await nextTick() + expect(root.innerHTML).toBe('
fallback
') show.value = true await nextTick() @@ -1539,96 +1094,16 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VdomForwardedSlotWithFallback = { - render(this: any) { - return h(VdomSlot, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VaporForwardedSlot2 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomForwardedSlotWithFallback as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot1 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot1, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VdomForwardedSlotWithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom fallback', + ) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlotWithFallback, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1653,43 +1128,9 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VdomForwardedSlot = { - render(this: any) { - return h(VaporSlot as any, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const App = { - setup() { - return () => - h( - VdomForwardedSlot, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1712,60 +1153,10 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VdomForwardedSlot = { - render(this: any) { - return h(VaporForwardedSlot as any, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const App = { - setup() { - return () => - h( - VdomForwardedSlot, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const VdomForwardedSlot = createVdomForwardedSlot(VaporForwardedSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1788,78 +1179,13 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VdomForwardedSlot2 = { - render(this: any) { - return h(VaporForwardedSlot as any, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot1 = { - render(this: any) { - return h(VdomForwardedSlot2, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot = { - render(this: any) { - return h(VdomForwardedSlot1, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const App = { - setup() { - return () => - h( - VdomForwardedSlot, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot) + const VdomForwardedSlot = createMultipleVdomForwardedSlots( + VaporForwardedSlot, + 3, + ) + const App = createTestApp(VdomForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1882,81 +1208,16 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VaporForwardedSlot = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const VdomForwardedSlot2 = { - render(this: any) { - return h(VaporForwardedSlot as any, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot1 = { - render(this: any) { - return h(VdomForwardedSlot2, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot = { - render(this: any) { - return h(VdomForwardedSlot1, null, { - foo: () => [renderSlot(this.$slots, 'foo')], - _: 3 /* FORWARDED */, - }) - }, - } - - const App = { - setup() { - return () => - h( - VdomForwardedSlot, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VaporForwardedSlot = createVaporForwardedSlot( + VaporSlot, + 'vapor fallback', + ) + const VdomForwardedSlot = createMultipleVdomForwardedSlots( + VaporForwardedSlot, + 3, + ) + const App = createTestApp(VdomForwardedSlot, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -1979,66 +1240,12 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VaporForwardedSlot2 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot1 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot1 as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VaporForwardedSlot1 = createMultipleVaporForwardedSlots( + VdomSlot, + 2, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -2061,69 +1268,13 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VaporForwardedSlot2 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot1WithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor1 fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot1WithFallback as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2 = createVaporForwardedSlot(VdomSlot) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -2146,69 +1297,15 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VaporForwardedSlot2WithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor2 fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot1 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2WithFallback, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot1 as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VdomSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1 = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -2222,78 +1319,21 @@ describe('component: slots', () => { await nextTick() expect(root.innerHTML).toBe('
vapor2 fallback
') - show.value = true - await nextTick() - expect(root.innerHTML).toBe('bar') - }) - - test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => { - const foo = ref('foo') - const show = ref(true) - - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VaporForwardedSlot2 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporSlot, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot1 = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2, - null, - { - foo: () => { - return createForwardedSlot('foo', null) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot1 as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } - + show.value = true + await nextTick() + expect(root.innerHTML).toBe('bar') + }) + + test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => { + const foo = ref('foo') + const show = ref(true) + + const VaporSlot = createVaporSlot() + const VaporForwardedSlot2 = createVaporForwardedSlot(VaporSlot) + const VaporForwardedSlot1 = + createVaporForwardedSlot(VaporForwardedSlot2) + const App = createTestApp(VaporForwardedSlot1, foo, show) + const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) expect(root.innerHTML).toBe('foo') @@ -2315,72 +1355,16 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VaporForwardedSlot2WithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VdomSlot as any, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor2 fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot1WithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2WithFallback, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor1 fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot1WithFallback as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VdomSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -2403,74 +1387,16 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VaporForwardedSlot2WithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporSlot, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor2 fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const VaporForwardedSlot1WithFallback = defineVaporComponent({ - setup() { - const createForwardedSlot = forwardedSlotCreator() - const n2 = createComponent( - VaporForwardedSlot2WithFallback, - null, - { - foo: () => { - return createForwardedSlot('foo', null, () => { - const n2 = template('
vapor1 fallback
')() - return n2 - }) - }, - }, - true, - ) - return n2 - }, - }) - - const App = { - setup() { - return () => - h( - VaporForwardedSlot1WithFallback as any, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VaporForwardedSlot2WithFallback = createVaporForwardedSlot( + VaporSlot, + 'vapor2 fallback', + ) + const VaporForwardedSlot1WithFallback = createVaporForwardedSlot( + VaporForwardedSlot2WithFallback, + 'vapor1 fallback', + ) + const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -2495,60 +1421,16 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VdomForwardedSlot2WithFallback = { - render(this: any) { - return h(VaporSlot as any, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom2 fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot1WithFallback = { - render(this: any) { - return h(VdomForwardedSlot2WithFallback, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom1 fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const App = { - setup() { - return () => - h( - VdomForwardedSlot1WithFallback, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -2571,58 +1453,16 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VdomSlot = { - render(this: any) { - return renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'fallback'), - ]) - }, - } - - const VdomForwardedSlot2WithFallback = { - render(this: any) { - return h(VdomSlot, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom2 fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot1WithFallback = { - render(this: any) { - return h(VdomForwardedSlot2WithFallback, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom1 fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const App = { - setup() { - return () => - h( - VdomForwardedSlot1WithFallback, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VdomSlot = createVdomSlot() + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VdomSlot, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) @@ -2645,73 +1485,20 @@ describe('component: slots', () => { const foo = ref('foo') const show = ref(true) - const VaporSlot = defineVaporComponent({ - setup() { - const n0 = createSlot('foo', null, () => { - const n2 = template('
fallback
')() - return n2 - }) - return n0 - }, - }) - - const VdomForwardedSlot3WithFallback = { - render(this: any) { - return h(VaporSlot as any, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom3 fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot2WithFallback = { - render(this: any) { - return h(VdomForwardedSlot3WithFallback, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom2 fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const VdomForwardedSlot1WithFallback = { - render(this: any) { - return h(VdomForwardedSlot2WithFallback, null, { - foo: () => [ - renderSlot(this.$slots, 'foo', {}, () => [ - h('div', 'vdom1 fallback'), - ]), - ], - _: 3 /* FORWARDED */, - }) - }, - } - - const App = { - setup() { - return () => - h( - VdomForwardedSlot1WithFallback, - null, - createSlots({ _: 2 /* DYNAMIC */ } as any, [ - show.value - ? { - name: 'foo', - fn: () => [h('span', foo.value)], - key: '0', - } - : undefined, - ]), - ) - }, - } + const VaporSlot = createVaporSlot() + const VdomForwardedSlot3WithFallback = createVdomForwardedSlot( + VaporSlot, + 'vdom3 fallback', + ) + const VdomForwardedSlot2WithFallback = createVdomForwardedSlot( + VdomForwardedSlot3WithFallback, + 'vdom2 fallback', + ) + const VdomForwardedSlot1WithFallback = createVdomForwardedSlot( + VdomForwardedSlot2WithFallback, + 'vdom1 fallback', + ) + const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show) const root = document.createElement('div') createApp(App).use(vaporInteropPlugin).mount(root) From 4435a2eca543f02b56ebfea6c79fca8b4a1a4dba Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 13 Jun 2025 14:55:06 +0800 Subject: [PATCH 07/16] chore: use fragment's anchor as insertion point --- .../__tests__/componentSlots.spec.ts | 48 +++++++++---------- packages/runtime-vapor/src/block.ts | 5 +- packages/runtime-vapor/src/vdomInterop.ts | 14 +++--- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 4af9ef74eec..9528e91de18 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -796,7 +796,7 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
fallback
') + expect(root.innerHTML).toBe('
fallback
') }) test('vdom slot > vapor forwarded slot(with fallback) > vapor slot', async () => { @@ -820,7 +820,7 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
forwarded fallback
') + expect(root.innerHTML).toBe('
forwarded fallback
') }) test('vdom slot > vapor forwarded slot > vdom slot', async () => { @@ -1083,11 +1083,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
fallback
') + expect(root.innerHTML).toBe('
fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot(with fallback) > vdom slot', async () => { @@ -1116,12 +1116,12 @@ describe('component: slots', () => { show.value = false await nextTick() expect(root.innerHTML).toBe( - '
vdom fallback
', + '
vdom fallback
', ) show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vdom forwarded slot > vapor slot', async () => { @@ -1168,11 +1168,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
fallback
') + expect(root.innerHTML).toBe('
fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot > vdom slot', async () => { @@ -1197,11 +1197,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
fallback
') + expect(root.innerHTML).toBe('
fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot(with fallback) > vdom slot', async () => { @@ -1229,11 +1229,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
vapor fallback
') + expect(root.innerHTML).toBe('
vapor fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vapor forwarded slot > vapor forwarded slot > vdom slot', async () => { @@ -1257,11 +1257,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
fallback
') + expect(root.innerHTML).toBe('
fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot > vdom slot', async () => { @@ -1286,11 +1286,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
vapor1 fallback
') + expect(root.innerHTML).toBe('
vapor1 fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vapor forwarded slot > vapor forwarded slot(with fallback) > vdom slot', async () => { @@ -1317,11 +1317,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
vapor2 fallback
') + expect(root.innerHTML).toBe('
vapor2 fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => { @@ -1344,11 +1344,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
fallback
') + expect(root.innerHTML).toBe('
fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vdom slot', async () => { @@ -1376,11 +1376,11 @@ describe('component: slots', () => { show.value = false await nextTick() - expect(root.innerHTML).toBe('
vapor1 fallback
') + expect(root.innerHTML).toBe('
vapor1 fallback
') show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vapor slot', async () => { @@ -1409,12 +1409,12 @@ describe('component: slots', () => { show.value = false await nextTick() expect(root.innerHTML).toBe( - '
vapor1 fallback
', + '
vapor1 fallback
', ) show.value = true await nextTick() - expect(root.innerHTML).toBe('bar') + expect(root.innerHTML).toBe('bar') }) test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vapor slot', async () => { diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index b2d0ca04b67..93c88f9c011 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -124,6 +124,10 @@ export function insert( insert(b, parent, anchor) } } else { + if (block.anchor) { + insert(block.anchor, parent, anchor) + anchor = block.anchor + } // fragment if (block.insert) { // TODO handle hydration for vdom interop @@ -131,7 +135,6 @@ export function insert( } else { insert(block.nodes, parent, anchor) } - if (block.anchor) insert(block.anchor, parent, anchor) } } diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index a6b115f7474..9b73e15b84c 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -102,16 +102,10 @@ const vaporInteropImpl: Omit< slot(n1: VNode, n2: VNode, container, anchor) { if (!n1) { // mount - const selfAnchor = (n2.el = n2.anchor = createTextNode()) - insert(selfAnchor, container, anchor) + let selfAnchor: Node | undefined const { slot, fallback } = n2.vs! const propsRef = (n2.vs!.ref = shallowRef(n2.props)) const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler)) - // TODO fallback for slot with v-if content - // fallback is a vnode slot function here, and slotBlock, if a DynamicFragment, - // expects a Vapor BlockFn as fallback - // fallback - // forwarded vdom slot without its own fallback, use the fallback provided by // the slot outlet if (slotBlock instanceof DynamicFragment) { @@ -121,11 +115,15 @@ const vaporInteropImpl: Omit< ensureVDOMSlotFallback(nodes, fallback) nodes = nodes.nodes } + // use fragment's anchor when possible + selfAnchor = slotBlock.anchor } else if (isFragment(slotBlock)) { ensureVDOMSlotFallback(slotBlock, fallback) + selfAnchor = slotBlock.anchor! } - // TODO use fragment's anchor as selfAnchor? + if (!selfAnchor) selfAnchor = createTextNode() + insert((n2.el = n2.anchor = selfAnchor), container, anchor) insert((n2.vb = slotBlock), container, selfAnchor) } else { // update From 2ba4dc0d0720b0cc8639e04680dd157c71913299 Mon Sep 17 00:00:00 2001 From: daiwei Date: Mon, 21 Jul 2025 08:29:31 +0800 Subject: [PATCH 08/16] fix(runtime-vapor): render slot fallback if slot content is not a valid block close #13668 --- .../__tests__/componentSlots.spec.ts | 59 +++++++++++++++++++ packages/runtime-vapor/src/block.ts | 12 +++- packages/runtime-vapor/src/componentSlots.ts | 1 + 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 58076fff9ee..f54137724cc 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -502,5 +502,64 @@ describe('component: slots', () => { await nextTick() expect(host.innerHTML).toBe('

') }) + + test('render fallback when slot content is not valid', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return template('')() + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + }) + + test('render fallback when v-if condition is false', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const toggle = ref(false) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return createIf( + () => toggle.value, + () => { + return document.createTextNode('content') + }, + ) + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + toggle.value = true + await nextTick() + expect(html()).toBe('content') + + toggle.value = false + await nextTick() + expect(html()).toBe('fallback') + }) }) }) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index e021ce84b05..50a453dc483 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -67,9 +67,15 @@ export class DynamicFragment extends VaporFragment { if (this.fallback && !isValidBlock(this.nodes)) { parent && remove(this.nodes, parent) - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] + // if current nodes is a DynamicFragment, call its update with the fallback + // to handle nested dynamic fragment + if (this.nodes instanceof DynamicFragment) { + this.nodes.update(this.fallback) + } else { + this.nodes = + (this.scope || (this.scope = new EffectScope())).run(this.fallback) || + [] + } parent && insert(this.nodes, parent, this.anchor) } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 100c99cdb8a..4215548f2e6 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -126,6 +126,7 @@ export function createSlot( const renderSlot = () => { const slot = getSlot(rawSlots, isFunction(name) ? name() : name) if (slot) { + fragment.fallback = fallback // create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot fragment.update( From e92244e719788db07803c100eb3c88b7ba8b6cb6 Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 22 Jul 2025 10:22:19 +0800 Subject: [PATCH 09/16] chore: tweaks --- packages/runtime-vapor/src/block.ts | 28 ++++++++++++-- packages/runtime-vapor/src/componentSlots.ts | 40 ++------------------ 2 files changed, 28 insertions(+), 40 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 0b74f2f5259..def2ef3bf0c 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -67,9 +67,13 @@ export class DynamicFragment extends VaporFragment { if (this.fallback && !isValidBlock(this.nodes)) { parent && remove(this.nodes, parent) - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] + if (isFragment(this.nodes)) { + renderFallback(this.nodes, this.fallback, key) + } else { + this.nodes = + (this.scope || (this.scope = new EffectScope())).run(this.fallback) || + [] + } parent && insert(this.nodes, parent, this.anchor) } @@ -77,6 +81,24 @@ export class DynamicFragment extends VaporFragment { } } +function renderFallback( + fragment: VaporFragment, + fallback: BlockFn, + key: any, +): void { + if (fragment instanceof DynamicFragment) { + const nodes = fragment.nodes + if (isFragment(nodes)) { + renderFallback(nodes, fallback, key) + } else { + if (!fragment.fallback) fragment.fallback = fallback + fragment.update(fragment.fallback, key) + } + } else if (!fragment.fallback) { + fragment.fallback = fallback + } +} + export function isFragment(val: NonNullable): val is VaporFragment { return val instanceof VaporFragment } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 5ccbe6be89c..c5d2219606b 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,12 +1,5 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' -import { - type Block, - type BlockFn, - DynamicFragment, - type VaporFragment, - insert, - isFragment, -} from './block' +import { type Block, type BlockFn, DynamicFragment, insert } from './block' import { rawPropsProxyHandlers } from './componentProps' import { currentInstance, isRef } from '@vue/runtime-dom' import type { LooseRawProps, VaporComponentInstance } from './component' @@ -144,6 +137,7 @@ export function createSlot( const renderSlot = () => { const slot = getSlot(rawSlots, isFunction(name) ? name() : name) if (slot) { + fragment.fallback = fallback // create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot fragment.update( @@ -151,27 +145,8 @@ export function createSlot( (slot._bound = () => { const slotContent = slot(slotProps) if (slotContent instanceof DynamicFragment) { - let nodes = slotContent.nodes - if ( - (slotContent.fallback = fallback) && - isArray(nodes) && - nodes.length === 0 - ) { - // use fallback if the slot content is invalid - slotContent.update(fallback) - } else { - while (isFragment(nodes)) { - ensureVaporSlotFallback(nodes, fallback) - nodes = nodes.nodes - } - } - } - // forwarded vdom slot, if there is no fallback provide, try use the fallback - // provided by the slot outlet. - else if (isFragment(slotContent)) { - ensureVaporSlotFallback(slotContent, fallback) + slotContent.fallback = fallback } - return slotContent }), ) @@ -194,12 +169,3 @@ export function createSlot( return fragment } - -function ensureVaporSlotFallback( - block: VaporFragment, - fallback?: VaporSlot, -): void { - if (block.insert && !block.fallback && fallback) { - block.fallback = fallback - } -} From e28b96bea872fe06f9b6cb0a121403063fa1eccb Mon Sep 17 00:00:00 2001 From: daiwei Date: Tue, 22 Jul 2025 10:31:16 +0800 Subject: [PATCH 10/16] chore: tweaks --- .../__tests__/componentSlots.spec.ts | 59 +++++++++++++++++++ packages/runtime-vapor/src/block.ts | 23 ++++++-- 2 files changed, 78 insertions(+), 4 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index f54137724cc..608a0ff5568 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -561,5 +561,64 @@ describe('component: slots', () => { await nextTick() expect(html()).toBe('fallback') }) + + test('render fallback with nested v-if ', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const outerShow = ref(false) + const innerShow = ref(false) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return createIf( + () => outerShow.value, + () => { + return createIf( + () => innerShow.value, + () => { + return document.createTextNode('content') + }, + ) + }, + ) + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + outerShow.value = true + await nextTick() + expect(html()).toBe('fallback') + + innerShow.value = true + await nextTick() + expect(html()).toBe('content') + + innerShow.value = false + await nextTick() + expect(html()).toBe('fallback') + + outerShow.value = false + await nextTick() + expect(html()).toBe('fallback') + + outerShow.value = true + await nextTick() + expect(html()).toBe('fallback') + + innerShow.value = true + await nextTick() + expect(html()).toBe('content') + }) }) }) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 50a453dc483..2a78048f8b2 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -67,10 +67,9 @@ export class DynamicFragment extends VaporFragment { if (this.fallback && !isValidBlock(this.nodes)) { parent && remove(this.nodes, parent) - // if current nodes is a DynamicFragment, call its update with the fallback - // to handle nested dynamic fragment - if (this.nodes instanceof DynamicFragment) { - this.nodes.update(this.fallback) + // handle nested dynamic fragment + if (isFragment(this.nodes)) { + renderFallback(this.nodes, this.fallback, key) } else { this.nodes = (this.scope || (this.scope = new EffectScope())).run(this.fallback) || @@ -83,6 +82,22 @@ export class DynamicFragment extends VaporFragment { } } +function renderFallback( + fragment: VaporFragment, + fallback: BlockFn, + key: any, +): void { + if (fragment instanceof DynamicFragment) { + const nodes = fragment.nodes + if (isFragment(nodes)) { + renderFallback(nodes, fallback, key) + } else { + if (!fragment.fallback) fragment.fallback = fallback + fragment.update(fragment.fallback, key) + } + } +} + export function isFragment(val: NonNullable): val is VaporFragment { return val instanceof VaporFragment } From a65da3aee915014b9de5652481a290d87b517d3f Mon Sep 17 00:00:00 2001 From: daiwei Date: Wed, 23 Jul 2025 18:07:22 +0800 Subject: [PATCH 11/16] wip: render fallback nodes for vfor --- .../__tests__/componentSlots.spec.ts | 109 +++++++++++++++++- packages/runtime-vapor/src/apiCreateFor.ts | 28 ++++- packages/runtime-vapor/src/block.ts | 56 +++++---- 3 files changed, 162 insertions(+), 31 deletions(-) diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 608a0ff5568..ea9a3f8c8d7 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -1,7 +1,9 @@ // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`. import { + child, createComponent, + createFor, createForSlots, createIf, createSlot, @@ -12,10 +14,15 @@ import { renderEffect, template, } from '../src' -import { currentInstance, nextTick, ref } from '@vue/runtime-dom' +import { + currentInstance, + nextTick, + ref, + toDisplayString, +} from '@vue/runtime-dom' import { makeRender } from './_utils' import type { DynamicSlot } from '../src/componentSlots' -import { setElementText } from '../src/dom/prop' +import { setElementText, setText } from '../src/dom/prop' const define = makeRender() @@ -562,7 +569,7 @@ describe('component: slots', () => { expect(html()).toBe('fallback') }) - test('render fallback with nested v-if ', async () => { + test('render fallback with nested v-if', async () => { const Child = { setup() { return createSlot('default', null, () => @@ -620,5 +627,101 @@ describe('component: slots', () => { await nextTick() expect(html()).toBe('content') }) + + test('render fallback with v-for', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const items = ref([1]) + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + const n2 = createFor( + () => items.value, + for_item0 => { + const n4 = template(' ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(for_item0.value)), + ) + return n4 + }, + ) + return n2 + }, + }) + }, + }).render() + + expect(html()).toBe('1') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.push(2) + await nextTick() + expect(html()).toBe('2') + }) + + test('render fallback with v-for (empty source)', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const items = ref([]) + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + const n2 = createFor( + () => items.value, + for_item0 => { + const n4 = template(' ')() as any + const x4 = child(n4) as any + renderEffect(() => + setText(x4, toDisplayString(for_item0.value)), + ) + return n4 + }, + ) + return n2 + }, + }) + }, + }).render() + + expect(html()).toBe('fallback') + + items.value.push(1) + await nextTick() + expect(html()).toBe('1') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.pop() + await nextTick() + expect(html()).toBe('fallback') + + items.value.push(2) + await nextTick() + expect(html()).toBe('2') + }) }) }) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 9ffdf6dca57..ac2a94f0926 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -15,8 +15,10 @@ import { isArray, isObject, isString } from '@vue/shared' import { createComment, createTextNode } from './dom/node' import { type Block, + ForFragment, VaporFragment, insert, + remove, remove as removeBlock, } from './block' import { warn } from '@vue/runtime-dom' @@ -77,7 +79,7 @@ export const createFor = ( setup?: (_: { createSelector: (source: () => any) => (cb: () => void) => void }) => void, -): VaporFragment => { +): ForFragment => { const _insertionParent = insertionParent const _insertionAnchor = insertionAnchor if (isHydrating) { @@ -94,7 +96,7 @@ export const createFor = ( let currentKey: any // TODO handle this in hydration const parentAnchor = __DEV__ ? createComment('for') : createTextNode() - const frag = new VaporFragment(oldBlocks) + const frag = new ForFragment(oldBlocks) const instance = currentInstance! const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE) const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT) @@ -112,6 +114,7 @@ export const createFor = ( const newLength = source.values.length const oldLength = oldBlocks.length newBlocks = new Array(newLength) + let isFallback = false const prevSub = setActiveSub() @@ -123,6 +126,11 @@ export const createFor = ( } else { parent = parent || parentAnchor!.parentNode if (!oldLength) { + // remove fallback nodes + if (frag.fallback && (frag.nodes[0] as Block[]).length > 0) { + remove(frag.nodes[0], parent!) + } + // fast path for all new for (let i = 0; i < newLength; i++) { mount(source, i) @@ -140,6 +148,13 @@ export const createFor = ( parent!.textContent = '' parent!.appendChild(parentAnchor) } + + // render fallback nodes + if (frag.fallback) { + insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor) + oldBlocks = [] + isFallback = true + } } else if (!getKey) { // unkeyed fast path const commonLength = Math.min(newLength, oldLength) @@ -324,11 +339,12 @@ export const createFor = ( } } - frag.nodes = [(oldBlocks = newBlocks)] - if (parentAnchor) { - frag.nodes.push(parentAnchor) + if (!isFallback) { + frag.nodes = [(oldBlocks = newBlocks)] + if (parentAnchor) { + frag.nodes.push(parentAnchor) + } } - setActiveSub(prevSub) } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 2a78048f8b2..0271c1c0087 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -18,17 +18,24 @@ export type Block = export type BlockFn = (...args: any[]) => Block -export class VaporFragment { - nodes: Block +export class VaporFragment { + nodes: T anchor?: Node insert?: (parent: ParentNode, anchor: Node | null) => void remove?: (parent?: ParentNode) => void + fallback?: BlockFn - constructor(nodes: Block) { + constructor(nodes: T) { this.nodes = nodes } } +export class ForFragment extends VaporFragment { + constructor(nodes: Block[]) { + super(nodes) + } +} + export class DynamicFragment extends VaporFragment { anchor: Node scope: EffectScope | undefined @@ -65,16 +72,18 @@ export class DynamicFragment extends VaporFragment { this.nodes = [] } - if (this.fallback && !isValidBlock(this.nodes)) { + if (this.fallback) { parent && remove(this.nodes, parent) - // handle nested dynamic fragment - if (isFragment(this.nodes)) { - renderFallback(this.nodes, this.fallback, key) - } else { - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] - } + const scope = this.scope || (this.scope = new EffectScope()) + scope.run(() => { + // handle nested fragment + if (isFragment(this.nodes)) { + ensureFallback(this.nodes, this.fallback!) + } else if (!isValidBlock(this.nodes)) { + this.nodes = this.fallback!() || [] + } + }) + parent && insert(this.nodes, parent, this.anchor) } @@ -82,19 +91,22 @@ export class DynamicFragment extends VaporFragment { } } -function renderFallback( - fragment: VaporFragment, - fallback: BlockFn, - key: any, -): void { +function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void { + if (!fragment.fallback) fragment.fallback = fallback + if (fragment instanceof DynamicFragment) { const nodes = fragment.nodes if (isFragment(nodes)) { - renderFallback(nodes, fallback, key) - } else { - if (!fragment.fallback) fragment.fallback = fallback - fragment.update(fragment.fallback, key) + ensureFallback(nodes, fallback) + } else if (!isValidBlock(nodes)) { + fragment.update(fragment.fallback) } + } else if (fragment instanceof ForFragment) { + if (!isValidBlock(fragment.nodes[0])) { + fragment.nodes[0] = [fallback() || []] as Block[] + } + } else { + // vdom slots } } @@ -117,7 +129,7 @@ export function isValidBlock(block: Block): boolean { } else if (isVaporComponent(block)) { return isValidBlock(block.block) } else if (isArray(block)) { - return block.length > 0 && block.every(isValidBlock) + return block.length > 0 && block.some(isValidBlock) } else { // fragment return isValidBlock(block.nodes) From 1ab9cc10f17a2de5c259b7c1390651d38cfc918e Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 25 Jul 2025 10:16:29 +0800 Subject: [PATCH 12/16] chore: tweaks --- packages/runtime-vapor/src/block.ts | 50 ++++++++++---------- packages/runtime-vapor/src/componentSlots.ts | 11 +---- 2 files changed, 26 insertions(+), 35 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 0271c1c0087..90070762dba 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -73,38 +73,38 @@ export class DynamicFragment extends VaporFragment { } if (this.fallback) { - parent && remove(this.nodes, parent) - const scope = this.scope || (this.scope = new EffectScope()) - scope.run(() => { - // handle nested fragment - if (isFragment(this.nodes)) { - ensureFallback(this.nodes, this.fallback!) - } else if (!isValidBlock(this.nodes)) { - this.nodes = this.fallback!() || [] + // set fallback for nested fragments + const isFrag = isFragment(this.nodes) + if (isFrag) { + if (!(this.nodes as VaporFragment).fallback) { + ;(this.nodes as VaporFragment).fallback = this.fallback } - }) - - parent && insert(this.nodes, parent, this.anchor) + } + + if (!isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent) + const scope = this.scope || (this.scope = new EffectScope()) + scope.run(() => { + if (isFrag) { + // render fragment's fallback + renderFragmentFallback(this.nodes as VaporFragment) + } else { + this.nodes = this.fallback!() || [] + } + }) + parent && insert(this.nodes, parent, this.anchor) + } } setActiveSub(prevSub) } } -function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void { - if (!fragment.fallback) fragment.fallback = fallback - - if (fragment instanceof DynamicFragment) { - const nodes = fragment.nodes - if (isFragment(nodes)) { - ensureFallback(nodes, fallback) - } else if (!isValidBlock(nodes)) { - fragment.update(fragment.fallback) - } - } else if (fragment instanceof ForFragment) { - if (!isValidBlock(fragment.nodes[0])) { - fragment.nodes[0] = [fallback() || []] as Block[] - } +function renderFragmentFallback(fragment: VaporFragment): void { + if (fragment instanceof ForFragment) { + fragment.nodes[0] = [fragment.fallback!() || []] as Block[] + } else if (fragment instanceof DynamicFragment) { + fragment.update(fragment.fallback) } else { // vdom slots } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 4215548f2e6..036cf670e40 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -129,16 +129,7 @@ export function createSlot( fragment.fallback = fallback // create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot - fragment.update( - slot._bound || - (slot._bound = () => { - const slotContent = slot(slotProps) - if (slotContent instanceof DynamicFragment) { - slotContent.fallback = fallback - } - return slotContent - }), - ) + fragment.update(slot._bound || (slot._bound = () => slot(slotProps))) } else { fragment.update(fallback) } From 30af803d9991dc525a07a607e1c5808faf9d20a7 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 25 Jul 2025 10:16:29 +0800 Subject: [PATCH 13/16] chore: tweaks --- packages/runtime-vapor/src/block.ts | 57 ++++++++++++-------- packages/runtime-vapor/src/componentSlots.ts | 11 +--- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 0271c1c0087..7c178e894b0 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -73,38 +73,53 @@ export class DynamicFragment extends VaporFragment { } if (this.fallback) { - parent && remove(this.nodes, parent) - const scope = this.scope || (this.scope = new EffectScope()) - scope.run(() => { - // handle nested fragment - if (isFragment(this.nodes)) { - ensureFallback(this.nodes, this.fallback!) - } else if (!isValidBlock(this.nodes)) { - this.nodes = this.fallback!() || [] - } - }) - - parent && insert(this.nodes, parent, this.anchor) + // set fallback for nested fragments + const isFrag = isFragment(this.nodes) + if (isFrag) { + setFragmentFallback(this.nodes as VaporFragment, this.fallback) + } + + if (!isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent) + const scope = this.scope || (this.scope = new EffectScope()) + scope.run(() => { + if (isFrag) { + // render fragment's fallback + renderFragmentFallback(this.nodes as VaporFragment) + } else { + this.nodes = this.fallback!() || [] + } + }) + parent && insert(this.nodes, parent, this.anchor) + } } setActiveSub(prevSub) } } -function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void { - if (!fragment.fallback) fragment.fallback = fallback +function setFragmentFallback( + fragment: VaporFragment, + fallback: BlockFn | undefined, +): void { + if (!fragment.fallback) { + fragment.fallback = fallback + } + if (isFragment(fragment.nodes)) { + setFragmentFallback(fragment.nodes, fallback) + } +} - if (fragment instanceof DynamicFragment) { +function renderFragmentFallback(fragment: VaporFragment): void { + if (fragment instanceof ForFragment) { + fragment.nodes[0] = [fragment.fallback!() || []] as Block[] + } else if (fragment instanceof DynamicFragment) { const nodes = fragment.nodes if (isFragment(nodes)) { - ensureFallback(nodes, fallback) - } else if (!isValidBlock(nodes)) { + renderFragmentFallback(nodes) + } else { fragment.update(fragment.fallback) } - } else if (fragment instanceof ForFragment) { - if (!isValidBlock(fragment.nodes[0])) { - fragment.nodes[0] = [fallback() || []] as Block[] - } } else { // vdom slots } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 4215548f2e6..036cf670e40 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -129,16 +129,7 @@ export function createSlot( fragment.fallback = fallback // create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot - fragment.update( - slot._bound || - (slot._bound = () => { - const slotContent = slot(slotProps) - if (slotContent instanceof DynamicFragment) { - slotContent.fallback = fallback - } - return slotContent - }), - ) + fragment.update(slot._bound || (slot._bound = () => slot(slotProps))) } else { fragment.update(fallback) } From b8ceb89a4634b46e619354610dc6df395dff0259 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 25 Jul 2025 10:16:29 +0800 Subject: [PATCH 14/16] chore: tweaks --- packages/runtime-vapor/src/apiCreateFor.ts | 7 ++- packages/runtime-vapor/src/block.ts | 57 ++++++++++++-------- packages/runtime-vapor/src/componentSlots.ts | 11 +--- 3 files changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/runtime-vapor/src/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index ac2a94f0926..0f032e685e9 100644 --- a/packages/runtime-vapor/src/apiCreateFor.ts +++ b/packages/runtime-vapor/src/apiCreateFor.ts @@ -152,7 +152,6 @@ export const createFor = ( // render fallback nodes if (frag.fallback) { insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor) - oldBlocks = [] isFallback = true } } else if (!getKey) { @@ -341,9 +340,9 @@ export const createFor = ( if (!isFallback) { frag.nodes = [(oldBlocks = newBlocks)] - if (parentAnchor) { - frag.nodes.push(parentAnchor) - } + if (parentAnchor) frag.nodes.push(parentAnchor) + } else { + oldBlocks = [] } setActiveSub(prevSub) } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 0271c1c0087..7c178e894b0 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -73,38 +73,53 @@ export class DynamicFragment extends VaporFragment { } if (this.fallback) { - parent && remove(this.nodes, parent) - const scope = this.scope || (this.scope = new EffectScope()) - scope.run(() => { - // handle nested fragment - if (isFragment(this.nodes)) { - ensureFallback(this.nodes, this.fallback!) - } else if (!isValidBlock(this.nodes)) { - this.nodes = this.fallback!() || [] - } - }) - - parent && insert(this.nodes, parent, this.anchor) + // set fallback for nested fragments + const isFrag = isFragment(this.nodes) + if (isFrag) { + setFragmentFallback(this.nodes as VaporFragment, this.fallback) + } + + if (!isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent) + const scope = this.scope || (this.scope = new EffectScope()) + scope.run(() => { + if (isFrag) { + // render fragment's fallback + renderFragmentFallback(this.nodes as VaporFragment) + } else { + this.nodes = this.fallback!() || [] + } + }) + parent && insert(this.nodes, parent, this.anchor) + } } setActiveSub(prevSub) } } -function ensureFallback(fragment: VaporFragment, fallback: BlockFn): void { - if (!fragment.fallback) fragment.fallback = fallback +function setFragmentFallback( + fragment: VaporFragment, + fallback: BlockFn | undefined, +): void { + if (!fragment.fallback) { + fragment.fallback = fallback + } + if (isFragment(fragment.nodes)) { + setFragmentFallback(fragment.nodes, fallback) + } +} - if (fragment instanceof DynamicFragment) { +function renderFragmentFallback(fragment: VaporFragment): void { + if (fragment instanceof ForFragment) { + fragment.nodes[0] = [fragment.fallback!() || []] as Block[] + } else if (fragment instanceof DynamicFragment) { const nodes = fragment.nodes if (isFragment(nodes)) { - ensureFallback(nodes, fallback) - } else if (!isValidBlock(nodes)) { + renderFragmentFallback(nodes) + } else { fragment.update(fragment.fallback) } - } else if (fragment instanceof ForFragment) { - if (!isValidBlock(fragment.nodes[0])) { - fragment.nodes[0] = [fallback() || []] as Block[] - } } else { // vdom slots } diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 4215548f2e6..036cf670e40 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -129,16 +129,7 @@ export function createSlot( fragment.fallback = fallback // create and cache bound version of the slot to make it stable // so that we avoid unnecessary updates if it resolves to the same slot - fragment.update( - slot._bound || - (slot._bound = () => { - const slotContent = slot(slotProps) - if (slotContent instanceof DynamicFragment) { - slotContent.fallback = fallback - } - return slotContent - }), - ) + fragment.update(slot._bound || (slot._bound = () => slot(slotProps))) } else { fragment.update(fallback) } From 5c6e533273c1728b115572633143ed02a2659838 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 25 Jul 2025 21:51:07 +0800 Subject: [PATCH 15/16] fix(runtime-vapor): improve fallback handling for nested fragments --- packages/runtime-vapor/src/block.ts | 35 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index 7c178e894b0..c2b08a98064 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -74,18 +74,19 @@ export class DynamicFragment extends VaporFragment { if (this.fallback) { // set fallback for nested fragments - const isFrag = isFragment(this.nodes) - if (isFrag) { + const hasNestedFragment = isFragment(this.nodes) + if (hasNestedFragment) { setFragmentFallback(this.nodes as VaporFragment, this.fallback) } - if (!isValidBlock(this.nodes)) { + const invalidFragment = findInvalidFragment(this) + if (invalidFragment) { parent && remove(this.nodes, parent) const scope = this.scope || (this.scope = new EffectScope()) scope.run(() => { - if (isFrag) { - // render fragment's fallback - renderFragmentFallback(this.nodes as VaporFragment) + // for nested fragments, render invalid fragment's fallback + if (hasNestedFragment) { + renderFragmentFallback(invalidFragment) } else { this.nodes = this.fallback!() || [] } @@ -102,9 +103,10 @@ function setFragmentFallback( fragment: VaporFragment, fallback: BlockFn | undefined, ): void { - if (!fragment.fallback) { - fragment.fallback = fallback - } + // stop recursion if fragment has its own fallback + if (fragment.fallback) return + + fragment.fallback = fallback if (isFragment(fragment.nodes)) { setFragmentFallback(fragment.nodes, fallback) } @@ -114,17 +116,20 @@ function renderFragmentFallback(fragment: VaporFragment): void { if (fragment instanceof ForFragment) { fragment.nodes[0] = [fragment.fallback!() || []] as Block[] } else if (fragment instanceof DynamicFragment) { - const nodes = fragment.nodes - if (isFragment(nodes)) { - renderFragmentFallback(nodes) - } else { - fragment.update(fragment.fallback) - } + fragment.update(fragment.fallback) } else { // vdom slots } } +function findInvalidFragment(fragment: VaporFragment): VaporFragment | null { + if (isValidBlock(fragment.nodes)) return null + + return isFragment(fragment.nodes) + ? findInvalidFragment(fragment.nodes) || fragment + : fragment +} + export function isFragment(val: NonNullable): val is VaporFragment { return val instanceof VaporFragment } From 5b4673fccdd36a4f55662f9ec339606ca7a78ad5 Mon Sep 17 00:00:00 2001 From: daiwei Date: Fri, 25 Jul 2025 22:15:47 +0800 Subject: [PATCH 16/16] chore: tweaks --- packages/runtime-vapor/src/block.ts | 4 ++-- packages/runtime-vapor/src/vdomInterop.ts | 24 ++++------------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index cdc562e32d3..9ccb0fb26a5 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -98,9 +98,9 @@ export class DynamicFragment extends VaporFragment { } } -function setFragmentFallback( +export function setFragmentFallback( fragment: VaporFragment, - fallback: BlockFn | undefined, + fallback: BlockFn, ): void { // stop recursion if fragment has its own fallback if (fragment.fallback) return diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 8dafdeaaf6a..eb579a23bea 100644 --- a/packages/runtime-vapor/src/vdomInterop.ts +++ b/packages/runtime-vapor/src/vdomInterop.ts @@ -35,11 +35,11 @@ import { } from './component' import { type Block, - DynamicFragment, VaporFragment, insert, isFragment, remove, + setFragmentFallback, } from './block' import { EMPTY_OBJ, extend, isArray, isFunction } from '@vue/shared' import { type RawProps, rawPropsProxyHandlers } from './componentProps' @@ -116,22 +116,12 @@ const vaporInteropImpl: Omit< const { slot, fallback } = n2.vs! const propsRef = (n2.vs!.ref = shallowRef(n2.props)) const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler)) - // forwarded vdom slot without its own fallback, use the fallback provided by - // the slot outlet - if (slotBlock instanceof DynamicFragment) { - // vapor slot's nodes is a forwarded vdom slot - let nodes = slotBlock.nodes - while (isFragment(nodes)) { - ensureVDOMSlotFallback(nodes, fallback) - nodes = nodes.nodes - } + // handle nested fragments + if (fallback && isFragment(slotBlock)) { + setFragmentFallback(slotBlock, createFallback(fallback)) // use fragment's anchor when possible selfAnchor = slotBlock.anchor - } else if (isFragment(slotBlock)) { - ensureVDOMSlotFallback(slotBlock, fallback) - selfAnchor = slotBlock.anchor! } - if (!selfAnchor) selfAnchor = createTextNode() insert((n2.el = n2.anchor = selfAnchor), container, anchor) insert((n2.vb = slotBlock), container, selfAnchor) @@ -356,12 +346,6 @@ export const vaporInteropPlugin: Plugin = app => { }) satisfies App['mount'] } -function ensureVDOMSlotFallback(block: VaporFragment, fallback?: () => any) { - if (block.insert && !block.fallback && fallback) { - block.fallback = createFallback(fallback) - } -} - const createFallback = (fallback: () => any) => (