From: daiwei Date: Wed, 11 Jun 2025 09:34:31 +0000 (+0800) Subject: wip: vdom slot interop X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=7cfec7fcfbc2801c785a445e1814e8e8319f3827;p=thirdparty%2Fvuejs%2Fcore.git wip: vdom slot interop --- diff --git a/packages/runtime-core/src/helpers/renderSlot.ts b/packages/runtime-core/src/helpers/renderSlot.ts index 152c5a4b81..c17cb1e310 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 e309554f2f..f921eb0a2c 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 5a18d62a8e..a146881678 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 bdbd960363..642be2a139 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 b782afd38d..b2d0ca04b6 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 19e9b5b6d1..b8af4c9162 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 e7c7e02e0b..a6b115f747 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 + }