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 f296d7257d0..a13da5abf9d 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 909162fe3ca..63b6b00010b 100644 --- a/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts +++ b/packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts @@ -420,6 +420,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/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 18f0139ab56..7dc9d2488ac 100644 --- a/packages/compiler-vapor/src/ir/index.ts +++ b/packages/compiler-vapor/src/ir/index.ts @@ -65,6 +65,7 @@ export interface RootIRNode { directive: Set block: BlockIRNode hasTemplateRef: boolean + hasForwardedSlot: boolean } export interface IfIRNode extends BaseIRNode { @@ -208,6 +209,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 946c89b734a..b79152f37b6 100644 --- a/packages/compiler-vapor/src/transform.ts +++ b/packages/compiler-vapor/src/transform.ts @@ -78,6 +78,7 @@ export class TransformContext { inVOnce: boolean = false inVFor: number = 0 + inSlot: boolean = false comment: CommentNode[] = [] component: Set = this.ir.component @@ -219,6 +220,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 e76fcdde65a..75d0c26f4af 100644 --- a/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts +++ b/packages/compiler-vapor/src/transforms/transformSlotOutlet.ts @@ -99,6 +99,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { } return () => { + if (context.inSlot) context.ir.hasForwardedSlot = true exitBlock && exitBlock() context.dynamic.operation = { type: IRNodeTypes.SLOT_OUTLET_NODE, @@ -106,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => { name: slotName, props: irProps, fallback, + forwarded: context.inSlot, } } } diff --git a/packages/compiler-vapor/src/transforms/vSlot.ts b/packages/compiler-vapor/src/transforms/vSlot.ts index 3e78913a23e..d9e379c772f 100644 --- a/packages/compiler-vapor/src/transforms/vSlot.ts +++ b/packages/compiler-vapor/src/transforms/vSlot.ts @@ -248,7 +248,14 @@ function createSlotBlock( const block: SlotBlockIRNode = newBlock(slotNode) block.props = dir && dir.exp const exitBlock = context.enterBlock(block) - return [block, exitBlock] + context.inSlot = true + return [ + block, + () => { + context.inSlot = false + exitBlock() + }, + ] } function isNonWhitespaceContent(node: TemplateChildNode): boolean { diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index d5feae617a6..f56f0aa0f3b 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 243bde548c5..a76368b3af7 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -558,6 +558,10 @@ export { startMeasure, endMeasure } from './profiling' * @internal */ export { initFeatureFlags } from './featureFlags' +/** + * @internal + */ +export { ensureVaporSlotFallback } from './helpers/renderSlot' /** * @internal */ diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 58076fff9ee..ea9c07dc025 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -1,21 +1,35 @@ // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`. import { + child, createComponent, + createFor, createForSlots, createIf, createSlot, createVaporApp, defineVaporComponent, + forwardedSlotCreator, insert, prepend, renderEffect, template, + vaporInteropPlugin, } from '../src' -import { currentInstance, nextTick, ref } from '@vue/runtime-dom' +import { + type Ref, + createApp, + createSlots, + currentInstance, + h, + nextTick, + ref, + renderSlot, + 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() @@ -469,6 +483,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) @@ -502,5 +553,2153 @@ 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') + }) + + 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') + }) + + 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') + }) + }) + + 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') + }) + + 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') + }) + + describe('vdom interop', () => { + const createVaporSlot = (fallbackText = 'fallback') => { + return defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + 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 createVaporForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + targetComponent, + null, + { + foo: () => { + return fallbackText + ? createForwardedSlot('foo', null, () => { + const n2 = template(`
${fallbackText}
`)() + return n2 + }) + : createForwardedSlot('foo', null) + }, + }, + true, + ) + 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 createTestApp = ( + rootComponent: any, + foo: Ref, + show: Ref, + ) => { + return { + setup() { + return () => + h( + rootComponent, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + } + + 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) + 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 = createVaporSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VaporSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + 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 = createVdomSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VdomSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + 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 = createVdomSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = createVdomSlot() + + 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) + 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 = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlot, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + 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 = 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) + 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 = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) + + 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 = 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) + 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 = 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) + 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 = 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) + 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 = createVdomSlot() + const VaporForwardedSlot1 = createMultipleVaporForwardedSlots( + VdomSlot, + 2, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + 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 = 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) + 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 = 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) + 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 = 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') + + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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') + }) + }) + }) + + 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') + }) + + 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') + }) + + describe('vdom interop', () => { + const createVaporSlot = (fallbackText = 'fallback') => { + return defineVaporComponent({ + setup() { + const n0 = createSlot('foo', null, () => { + 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 createVaporForwardedSlot = ( + targetComponent: any, + fallbackText?: string, + ) => { + return defineVaporComponent({ + setup() { + const createForwardedSlot = forwardedSlotCreator() + const n2 = createComponent( + targetComponent, + null, + { + foo: () => { + return fallbackText + ? createForwardedSlot('foo', null, () => { + const n2 = template(`
${fallbackText}
`)() + return n2 + }) + : createForwardedSlot('foo', null) + }, + }, + true, + ) + 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 createTestApp = ( + rootComponent: any, + foo: Ref, + show: Ref, + ) => { + return { + setup() { + return () => + h( + rootComponent, + null, + createSlots({ _: 2 /* DYNAMIC */ } as any, [ + show.value + ? { + name: 'foo', + fn: () => [h('span', foo.value)], + key: '0', + } + : undefined, + ]), + ) + }, + } + } + + 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) + 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 = createVaporSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VaporSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + 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 = createVdomSlot() + const VaporForwardedSlot = createVaporForwardedSlot(VdomSlot) + const App = createTestApp(VaporForwardedSlot, foo, show) + + 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 = createVdomSlot() + const VaporForwardedSlotWithFallback = createVaporForwardedSlot( + VdomSlot, + 'forwarded fallback', + ) + const App = createTestApp(VaporForwardedSlotWithFallback, foo, show) + + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = createVdomSlot() + + 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) + 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 = createVdomSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot) + const VaporForwardedSlot = createMultipleVaporForwardedSlots( + VdomForwardedSlot, + 3, + ) + const App = createTestApp(VaporForwardedSlot, foo, show) + + 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 = 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) + 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 = createVaporSlot() + const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot) + const App = createTestApp(VdomForwardedSlot, foo, show) + + 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 = 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) + 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 = 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) + 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 = 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) + 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 = createVdomSlot() + const VaporForwardedSlot1 = createMultipleVaporForwardedSlots( + VdomSlot, + 2, + ) + const App = createTestApp(VaporForwardedSlot1, foo, show) + + 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 = 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) + 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 = 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) + 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 = 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') + + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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 = 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) + 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/apiCreateFor.ts b/packages/runtime-vapor/src/apiCreateFor.ts index 9ffdf6dca57..0f032e685e9 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,12 @@ export const createFor = ( parent!.textContent = '' parent!.appendChild(parentAnchor) } + + // render fallback nodes + if (frag.fallback) { + insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor) + isFallback = true + } } else if (!getKey) { // unkeyed fast path const commonLength = Math.min(newLength, oldLength) @@ -324,11 +338,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) + } else { + oldBlocks = [] } - setActiveSub(prevSub) } diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index e021ce84b05..9ccb0fb26a5 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -18,22 +18,28 @@ 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 current?: BlockFn - fallback?: BlockFn constructor(anchorLabel?: string) { super([]) @@ -65,18 +71,64 @@ export class DynamicFragment extends VaporFragment { this.nodes = [] } - if (this.fallback && !isValidBlock(this.nodes)) { - parent && remove(this.nodes, parent) - this.nodes = - (this.scope || (this.scope = new EffectScope())).run(this.fallback) || - [] - parent && insert(this.nodes, parent, this.anchor) + if (this.fallback) { + // set fallback for nested fragments + const hasNestedFragment = isFragment(this.nodes) + if (hasNestedFragment) { + setFragmentFallback(this.nodes as VaporFragment, this.fallback) + } + + const invalidFragment = findInvalidFragment(this) + if (invalidFragment) { + parent && remove(this.nodes, parent) + const scope = this.scope || (this.scope = new EffectScope()) + scope.run(() => { + // for nested fragments, render invalid fragment's fallback + if (hasNestedFragment) { + renderFragmentFallback(invalidFragment) + } else { + this.nodes = this.fallback!() || [] + } + }) + parent && insert(this.nodes, parent, this.anchor) + } } setActiveSub(prevSub) } } +export function setFragmentFallback( + fragment: VaporFragment, + fallback: BlockFn, +): void { + // stop recursion if fragment has its own fallback + if (fragment.fallback) return + + fragment.fallback = fallback + if (isFragment(fragment.nodes)) { + setFragmentFallback(fragment.nodes, fallback) + } +} + +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 + } +} + +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 } @@ -96,7 +148,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) @@ -124,6 +176,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 +187,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/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 100c99cdb8a..78d823b3674 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -91,10 +91,21 @@ export function getSlot( } } +export function forwardedSlotCreator(): ( + name: string | (() => string), + rawProps?: LooseRawProps | null, + fallback?: VaporSlot, +) => Block { + const instance = currentInstance as VaporComponentInstance + return (name, rawProps, fallback) => + 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 @@ -104,7 +115,7 @@ export function createSlot( resetInsertionState() } - const instance = currentInstance as VaporComponentInstance + const instance = i || (currentInstance as VaporComponentInstance) const rawSlots = instance.rawSlots const slotProps = rawProps ? new Proxy(rawProps, rawPropsProxyHandlers) @@ -126,18 +137,10 @@ 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( - 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) } diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 7a8aea5a0d7..4900c8d7dd1 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -13,7 +13,7 @@ export { isVaporComponent, } 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 { diff --git a/packages/runtime-vapor/src/vdomInterop.ts b/packages/runtime-vapor/src/vdomInterop.ts index 1573a306922..eb579a23bea 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, @@ -13,7 +15,9 @@ import { createVNode, currentInstance, ensureRenderer, + ensureVaporSlotFallback, isEmitListener, + isVNode, onScopeDispose, renderSlot, shallowReactive, @@ -29,8 +33,15 @@ import { mountComponent, unmountComponent, } from './component' -import { type Block, VaporFragment, insert, remove } from './block' -import { EMPTY_OBJ, extend, isFunction } from '@vue/shared' +import { + type Block, + VaporFragment, + insert, + isFragment, + remove, + setFragmentFallback, +} from './block' +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' @@ -101,15 +112,18 @@ 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 + // handle nested fragments + if (fallback && isFragment(slotBlock)) { + setFragmentFallback(slotBlock, createFallback(fallback)) + // use fragment's anchor when possible + selfAnchor = slotBlock.anchor + } + if (!selfAnchor) selfAnchor = createTextNode() + insert((n2.el = n2.anchor = selfAnchor), container, anchor) insert((n2.vb = slotBlock), container, selfAnchor) } else { // update @@ -240,34 +254,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, - ) - if ((vnode.children as any[]).length) { + 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, + ) + + 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 + } + + 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 } @@ -309,3 +345,31 @@ export const vaporInteropPlugin: Plugin = app => { return mount(...args) }) satisfies App['mount'] } + +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 + }