]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: vdom slot interop
authordaiwei <daiwei521@126.com>
Wed, 11 Jun 2025 09:34:31 +0000 (17:34 +0800)
committerdaiwei <daiwei521@126.com>
Thu, 12 Jun 2025 07:16:59 +0000 (15:16 +0800)
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/index.ts
packages/runtime-core/src/renderer.ts
packages/runtime-vapor/__tests__/componentSlots.spec.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/vdomInterop.ts

index 152c5a4b81c13735825feb6e6d182ad08e2b0136..c17cb1e3105daca69e47935a0c407ce01f721c3b 100644 (file)
@@ -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
+    }
+  }
+}
index e309554f2f6c3edd517c9b736d06ee1c72594698..f921eb0a2cf8101695612f578f9d26e8cac54f3b 100644 (file)
@@ -557,3 +557,7 @@ export { startMeasure, endMeasure } from './profiling'
  * @internal
  */
 export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { ensureVaporSlotFallback } from './helpers/renderSlot'
index 5a18d62a8e1b2d6f74bdd814217bf482e4fa8ac2..a1468816789c002edd4885b31eab6ffe8eb67b21 100644 (file)
@@ -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
index bdbd960363dbc783267b26fc9f3e5c8759008ec4..642be2a139f2d4f4f6e3f9b0fd7f45f7d2ccf14b 100644 (file)
@@ -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<!--if--><!--slot-->')
     })
 
+    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<!--if--><!--slot-->')
+
+      toggle.value = true
+      await nextTick()
+      expect(html()).toBe('content<!--if--><!--slot-->')
+
+      toggle.value = false
+      await nextTick()
+      expect(html()).toBe('fallback<!--if--><!--slot-->')
+    })
+
     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('bar<!--slot--><!--slot-->bar<!--slot-->')
     })
+
+    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('<div>fallback</div>')()
+              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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>fallback</div>')
+      })
+
+      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('<div>fallback</div>')()
+              return n2
+            })
+            return n0
+          },
+        })
+
+        const VaporForwardedSlotWithFallback = defineVaporComponent({
+          setup() {
+            const createForwardedSlot = forwardedSlotCreator()
+            const n2 = createComponent(
+              VaporSlot,
+              null,
+              {
+                foo: () => {
+                  return createForwardedSlot('foo', null, () => {
+                    const n2 = template('<div>forwarded fallback</div>')()
+                    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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>forwarded fallback</div>')
+      })
+
+      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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<div>forwarded fallback</div>')()
+                    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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>forwarded fallback</div>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<div>forwarded fallback</div>')()
+                    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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>forwarded fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>vdom fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<div>vdom fallback</div>')
+      })
+
+      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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<div>vapor fallback</div>')()
+                    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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>vapor fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>vdom fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<span>foo</span><!--slot--><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><!--slot--><div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><!--slot--><span>bar</span>')
+      })
+
+      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('<span>foo</span><!--slot--><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe(
+          '<!--slot--><!--slot--><div>vdom fallback</div>',
+        )
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><!--slot--><span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<div>vapor fallback</div>')()
+                    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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>vapor fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><span>bar</span>')
+      })
+
+      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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><span>bar</span>')
+      })
+
+      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('<div>vapor1 fallback</div>')()
+                    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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>vapor1 fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><span>bar</span>')
+      })
+
+      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('<div>vapor2 fallback</div>')()
+                    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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>vapor2 fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span><!--slot--><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><!--slot--><div>fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><!--slot--><span>bar</span>')
+      })
+
+      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('<div>vapor2 fallback</div>')()
+                    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('<div>vapor1 fallback</div>')()
+                    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('<span>foo</span><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><div>vapor1 fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              return n2
+            })
+            return n0
+          },
+        })
+
+        const VaporForwardedSlot2WithFallback = defineVaporComponent({
+          setup() {
+            const createForwardedSlot = forwardedSlotCreator()
+            const n2 = createComponent(
+              VaporSlot,
+              null,
+              {
+                foo: () => {
+                  return createForwardedSlot('foo', null, () => {
+                    const n2 = template('<div>vapor2 fallback</div>')()
+                    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('<div>vapor1 fallback</div>')()
+                    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('<span>foo</span><!--slot--><!--slot-->')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe(
+          '<!--slot--><!--slot--><div>vapor1 fallback</div>',
+        )
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<!--slot--><!--slot--><span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>vdom1 fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>vdom1 fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+
+      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('<div>fallback</div>')()
+              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('<span>foo</span>')
+
+        foo.value = 'bar'
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+
+        show.value = false
+        await nextTick()
+        expect(root.innerHTML).toBe('<div>vdom1 fallback</div>')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span>')
+      })
+    })
   })
 })
index b782afd38d35b66c9fed33675d4f4705efbceb3f..b2d0ca04b67da88cc8c95ef05faae3686a5e0735 100644 (file)
@@ -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([])
index 19e9b5b6d1a9b51d9e1fa8df7d2dbf0f97c3cbdb..b8af4c91629be9e139137c688487e1ba563515ed 100644 (file)
@@ -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
+  }
+}
index e7c7e02e0bdb5bb904af487277d9f22325f6a5bb..a6b115f747494d519b080dd9548817987e23a1e8 100644 (file)
@@ -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<RendererNode, RendererElement>,
+    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
+  }