]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(vapor): forwarded slots (#13408)
authoredison <daiwei521@126.com>
Mon, 20 Oct 2025 06:34:54 +0000 (14:34 +0800)
committerGitHub <noreply@github.com>
Mon, 20 Oct 2025 06:34:54 +0000 (14:34 +0800)
16 files changed:
packages/compiler-vapor/__tests__/transforms/__snapshots__/vSlot.spec.ts.snap
packages/compiler-vapor/__tests__/transforms/vSlot.spec.ts
packages/compiler-vapor/src/generate.ts
packages/compiler-vapor/src/generators/slotOutlet.ts
packages/compiler-vapor/src/ir/index.ts
packages/compiler-vapor/src/transform.ts
packages/compiler-vapor/src/transforms/transformSlotOutlet.ts
packages/compiler-vapor/src/transforms/vSlot.ts
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-core/src/index.ts
packages/runtime-vapor/__tests__/componentSlots.spec.ts
packages/runtime-vapor/src/apiCreateFor.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/componentSlots.ts
packages/runtime-vapor/src/index.ts
packages/runtime-vapor/src/vdomInterop.ts

index d77f05ab032127b1f43a097e3b80ad5d30ccbfb4..c70f49cf05f913b6246e6d64acccef7ed24464db 100644 (file)
@@ -103,6 +103,97 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: transform slot > forwarded slots > <slot w/ nested component> 1`] = `
+"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _createForwardedSlot = _forwardedSlotCreator()
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n1 = _createComponentWithFallback(_component_Comp, null, {
+        "default": () => {
+          const n0 = _createForwardedSlot("default", null)
+          return n0
+        }
+      })
+      return n1
+    }
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > forwarded slots > <slot> tag only 1`] = `
+"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _createForwardedSlot = _forwardedSlotCreator()
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n0 = _createForwardedSlot("default", null)
+      return n0
+    }
+  }, true)
+  return n1
+}"
+`;
+
+exports[`compiler: transform slot > forwarded slots > <slot> tag w/ template 1`] = `
+"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _createForwardedSlot = _forwardedSlotCreator()
+  const _component_Comp = _resolveComponent("Comp")
+  const n2 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n0 = _createForwardedSlot("default", null)
+      return n0
+    }
+  }, true)
+  return n2
+}"
+`;
+
+exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-for 1`] = `
+"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createFor as _createFor, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _createForwardedSlot = _forwardedSlotCreator()
+  const _component_Comp = _resolveComponent("Comp")
+  const n3 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n0 = _createFor(() => (_ctx.b), (_for_item0) => {
+        const n2 = _createForwardedSlot("default", null)
+        return n2
+      })
+      return n0
+    }
+  }, true)
+  return n3
+}"
+`;
+
+exports[`compiler: transform slot > forwarded slots > <slot> tag w/ v-if 1`] = `
+"import { forwardedSlotCreator as _forwardedSlotCreator, resolveComponent as _resolveComponent, createIf as _createIf, createComponentWithFallback as _createComponentWithFallback } from 'vue';
+
+export function render(_ctx) {
+  const _createForwardedSlot = _forwardedSlotCreator()
+  const _component_Comp = _resolveComponent("Comp")
+  const n3 = _createComponentWithFallback(_component_Comp, null, {
+    "default": () => {
+      const n0 = _createIf(() => (_ctx.ok), () => {
+        const n2 = _createForwardedSlot("default", null)
+        return n2
+      })
+      return n0
+    }
+  }, true)
+  return n3
+}"
+`;
+
 exports[`compiler: transform slot > implicit default slot 1`] = `
 "import { resolveComponent as _resolveComponent, createComponentWithFallback as _createComponentWithFallback, template as _template } from 'vue';
 const t0 = _template("<div></div>")
index 909162fe3ca027e5716c6cb7f1b67672105f48f1..63b6b00010b187d96b7f680656c7dcd6031f641b 100644 (file)
@@ -420,6 +420,35 @@ describe('compiler: transform slot', () => {
     })
   })
 
+  describe('forwarded slots', () => {
+    test('<slot> tag only', () => {
+      const { code } = compileWithSlots(`<Comp><slot/></Comp>`)
+      expect(code).toMatchSnapshot()
+    })
+
+    test('<slot> tag w/ v-if', () => {
+      const { code } = compileWithSlots(`<Comp><slot v-if="ok"/></Comp>`)
+      expect(code).toMatchSnapshot()
+    })
+
+    test('<slot> tag w/ v-for', () => {
+      const { code } = compileWithSlots(`<Comp><slot v-for="a in b"/></Comp>`)
+      expect(code).toMatchSnapshot()
+    })
+
+    test('<slot> tag w/ template', () => {
+      const { code } = compileWithSlots(
+        `<Comp><template #default><slot/></template></Comp>`,
+      )
+      expect(code).toMatchSnapshot()
+    })
+
+    test('<slot w/ nested component>', () => {
+      const { code } = compileWithSlots(`<Comp><Comp><slot/></Comp></Comp>`)
+      expect(code).toMatchSnapshot()
+    })
+  })
+
   describe('errors', () => {
     test('error on extraneous children w/ named default slot', () => {
       const onError = vi.fn()
index 193a0f5da777be3f443ac9aa2480870aad6a3d34..ff3806611addf37b4f0dc8351306c8260bf22204 100644 (file)
@@ -18,6 +18,7 @@ import {
   genCall,
 } from './generators/utils'
 import { setTemplateRefIdent } from './generators/templateRef'
+import { createForwardedSlotIdent } from './generators/slotOutlet'
 
 export type CodegenOptions = Omit<BaseCodegenOptions, 'optimizeImports'>
 
@@ -129,6 +130,12 @@ export function generate(
       `const ${setTemplateRefIdent} = ${context.helper('createTemplateRefSetter')}()`,
     )
   }
+  if (ir.hasForwardedSlot) {
+    push(
+      NEWLINE,
+      `const ${createForwardedSlotIdent} = ${context.helper('forwardedSlotCreator')}()`,
+    )
+  }
   push(...genBlockContent(ir.block, context, true))
   push(INDENT_END, NEWLINE)
 
index 3221cbbd2c75e4f44a95ad6d903f29a195a91b95..dc992ae23347699bb65869bdc5f57dbec9c7e50e 100644 (file)
@@ -5,12 +5,14 @@ import { genExpression } from './expression'
 import { type CodeFragment, NEWLINE, buildCodeFragment, genCall } from './utils'
 import { genRawProps } from './component'
 
+export const createForwardedSlotIdent = `_createForwardedSlot`
+
 export function genSlotOutlet(
   oper: SlotOutletIRNode,
   context: CodegenContext,
 ): CodeFragment[] {
   const { helper } = context
-  const { id, name, fallback } = oper
+  const { id, name, fallback, forwarded } = oper
   const [frag, push] = buildCodeFragment()
 
   const nameExpr = name.isStatic
@@ -26,7 +28,7 @@ export function genSlotOutlet(
     NEWLINE,
     `const n${id} = `,
     ...genCall(
-      helper('createSlot'),
+      forwarded ? createForwardedSlotIdent : helper('createSlot'),
       nameExpr,
       genRawProps(oper.props, context) || 'null',
       fallbackArg,
index 69b6f25846a8844e7072299caf25b8a2b3f148c4..1018d7baa443cad5c661d97f6449307127f19d19 100644 (file)
@@ -67,6 +67,7 @@ export interface RootIRNode {
   directive: Set<string>
   block: BlockIRNode
   hasTemplateRef: boolean
+  hasForwardedSlot: boolean
 }
 
 export interface IfIRNode extends BaseIRNode {
@@ -213,6 +214,7 @@ export interface SlotOutletIRNode extends BaseIRNode {
   name: SimpleExpressionNode
   props: IRProps[]
   fallback?: BlockIRNode
+  forwarded?: boolean
   parent?: number
   anchor?: number
 }
index 946c89b734afecb32ebdc9a0c2a7c39a9f51f597..b79152f37b6dd88089ab5df225dc2413f5be1f3d 100644 (file)
@@ -78,6 +78,7 @@ export class TransformContext<T extends AllNode = AllNode> {
 
   inVOnce: boolean = false
   inVFor: number = 0
+  inSlot: boolean = false
 
   comment: CommentNode[] = []
   component: Set<string> = this.ir.component
@@ -219,6 +220,7 @@ export function transform(
     directive: new Set(),
     block: newBlock(node),
     hasTemplateRef: false,
+    hasForwardedSlot: false,
   }
 
   const context = new TransformContext(ir, node, options)
index e76fcdde65a7062c4f8bfe407b799b2505a30f38..75d0c26f4afb2af46c36c3a9b40c4beeabea56bb 100644 (file)
@@ -99,6 +99,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
   }
 
   return () => {
+    if (context.inSlot) context.ir.hasForwardedSlot = true
     exitBlock && exitBlock()
     context.dynamic.operation = {
       type: IRNodeTypes.SLOT_OUTLET_NODE,
@@ -106,6 +107,7 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
       name: slotName,
       props: irProps,
       fallback,
+      forwarded: context.inSlot,
     }
   }
 }
index 05aac4aee3c50cdfb86e693356d81d9f9bfa56eb..a71fa41150a481bfdcc5600a6ba5f4b7ac4e20fb 100644 (file)
@@ -269,7 +269,14 @@ function createSlotBlock(
     block.dynamic.needsKey = true
   }
   const exitBlock = context.enterBlock(block)
-  return [block, exitBlock]
+  context.inSlot = true
+  return [
+    block,
+    () => {
+      context.inSlot = false
+      exitBlock()
+    },
+  ]
 }
 
 function isNonWhitespaceContent(node: TemplateChildNode): boolean {
index d5feae617a639c1eee411064142dd77189216b64..f56f0aa0f3bde4637e8c4e74182e47c7ab84338f 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 dd63143077b64f29f50a763d5ccb728ad82931b5..5c86a4962acf2803ff2889260a169625af37da7f 100644 (file)
@@ -563,6 +563,10 @@ export { startMeasure, endMeasure } from './profiling'
  * @internal
  */
 export { initFeatureFlags } from './featureFlags'
+/**
+ * @internal
+ */
+export { ensureVaporSlotFallback } from './helpers/renderSlot'
 /**
  * @internal
  */
index ff7aa56d05529acd72b10b05d3c7878b1e5299a9..1beeecf2d08a8359cbdeaee00fb4d3f26ec97f7c 100644 (file)
@@ -1,22 +1,36 @@
 // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
 
 import {
+  child,
   createComponent,
+  createFor,
   createForSlots,
   createIf,
   createSlot,
   createVaporApp,
   defineVaporComponent,
+  forwardedSlotCreator,
   insert,
   prepend,
   renderEffect,
   setInsertionState,
   template,
+  vaporInteropPlugin,
 } from '../src'
-import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
+import {
+  type Ref,
+  createApp,
+  createSlots,
+  currentInstance,
+  h,
+  nextTick,
+  ref,
+  renderSlot,
+  toDisplayString,
+} from '@vue/runtime-dom'
 import { makeRender } from './_utils'
 import type { DynamicSlot } from '../src/componentSlots'
-import { setElementText } from '../src/dom/prop'
+import { setElementText, setText } from '../src/dom/prop'
 
 const define = makeRender<any>()
 
@@ -470,6 +484,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)
@@ -504,6 +555,1320 @@ describe('component: slots', () => {
       expect(host.innerHTML).toBe('<div><h1></h1><!--slot--></div>')
     })
 
+    test('render fallback when slot content is not valid', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              return template('<!--comment-->')()
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('fallback<!--slot-->')
+    })
+
+    test('render fallback when v-if condition is false', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const toggle = ref(false)
+
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              return createIf(
+                () => toggle.value,
+                () => {
+                  return document.createTextNode('content')
+                },
+              )
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('fallback<!--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('render fallback with nested v-if', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const outerShow = ref(false)
+      const innerShow = ref(false)
+
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              return createIf(
+                () => outerShow.value,
+                () => {
+                  return createIf(
+                    () => innerShow.value,
+                    () => {
+                      return document.createTextNode('content')
+                    },
+                  )
+                },
+              )
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('fallback<!--if--><!--slot-->')
+
+      outerShow.value = true
+      await nextTick()
+      expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')
+
+      innerShow.value = true
+      await nextTick()
+      expect(html()).toBe('content<!--if--><!--if--><!--slot-->')
+
+      innerShow.value = false
+      await nextTick()
+      expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')
+
+      outerShow.value = false
+      await nextTick()
+      expect(html()).toBe('fallback<!--if--><!--slot-->')
+
+      outerShow.value = true
+      await nextTick()
+      expect(html()).toBe('fallback<!--if--><!--if--><!--slot-->')
+
+      innerShow.value = true
+      await nextTick()
+      expect(html()).toBe('content<!--if--><!--if--><!--slot-->')
+    })
+
+    test('render fallback with v-for', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const items = ref<number[]>([1])
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              const n2 = createFor(
+                () => items.value,
+                for_item0 => {
+                  const n4 = template('<span> </span>')() as any
+                  const x4 = child(n4) as any
+                  renderEffect(() =>
+                    setText(x4, toDisplayString(for_item0.value)),
+                  )
+                  return n4
+                },
+              )
+              return n2
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.push(2)
+      await nextTick()
+      expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
+    })
+
+    test('render fallback with v-for (empty source)', async () => {
+      const Child = {
+        setup() {
+          return createSlot('default', null, () =>
+            document.createTextNode('fallback'),
+          )
+        },
+      }
+
+      const items = ref<number[]>([])
+      const { html } = define({
+        setup() {
+          return createComponent(Child, null, {
+            default: () => {
+              const n2 = createFor(
+                () => items.value,
+                for_item0 => {
+                  const n4 = template('<span> </span>')() as any
+                  const x4 = child(n4) as any
+                  renderEffect(() =>
+                    setText(x4, toDisplayString(for_item0.value)),
+                  )
+                  return n4
+                },
+              )
+              return n2
+            },
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.push(1)
+      await nextTick()
+      expect(html()).toBe('<span>1</span><!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('fallback<!--for--><!--slot-->')
+
+      items.value.push(2)
+      await nextTick()
+      expect(html()).toBe('<span>2</span><!--for--><!--slot-->')
+    })
+  })
+
+  describe('forwarded slot', () => {
+    test('should work', async () => {
+      const Child = defineVaporComponent({
+        setup() {
+          return createSlot('foo', null)
+        },
+      })
+      const Parent = defineVaporComponent({
+        setup() {
+          const createForwardedSlot = forwardedSlotCreator()
+          const n2 = createComponent(
+            Child,
+            null,
+            {
+              foo: () => {
+                return createForwardedSlot('foo', null)
+              },
+            },
+            true,
+          )
+          return n2
+        },
+      })
+
+      const foo = ref('foo')
+      const { host } = define({
+        setup() {
+          const n2 = createComponent(
+            Parent,
+            null,
+            {
+              foo: () => {
+                const n0 = template(' ')() as any
+                renderEffect(() => setText(n0, foo.value))
+                return n0
+              },
+            },
+            true,
+          )
+          return n2
+        },
+      }).render()
+
+      expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->')
+
+      foo.value = 'bar'
+      await nextTick()
+      expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->')
+    })
+
+    test('mixed with non-forwarded slot', async () => {
+      const Child = defineVaporComponent({
+        setup() {
+          return [createSlot('foo', null)]
+        },
+      })
+      const Parent = defineVaporComponent({
+        setup() {
+          const createForwardedSlot = forwardedSlotCreator()
+          const n2 = createComponent(Child, null, {
+            foo: () => {
+              const n0 = createForwardedSlot('foo', null)
+              return n0
+            },
+          })
+          const n3 = createSlot('default', null)
+          return [n2, n3]
+        },
+      })
+
+      const foo = ref('foo')
+      const { host } = define({
+        setup() {
+          const n2 = createComponent(
+            Parent,
+            null,
+            {
+              foo: () => {
+                const n0 = template(' ')() as any
+                renderEffect(() => setText(n0, foo.value))
+                return n0
+              },
+              default: () => {
+                const n3 = template(' ')() as any
+                renderEffect(() => setText(n3, foo.value))
+                return n3
+              },
+            },
+            true,
+          )
+          return n2
+        },
+      }).render()
+
+      expect(host.innerHTML).toBe('foo<!--slot--><!--slot-->foo<!--slot-->')
+
+      foo.value = 'bar'
+      await nextTick()
+      expect(host.innerHTML).toBe('bar<!--slot--><!--slot-->bar<!--slot-->')
+    })
+
+    test('forwarded slot with fallback', async () => {
+      const Child = defineVaporComponent({
+        setup() {
+          return createSlot('default', null, () => template('child fallback')())
+        },
+      })
+
+      const Parent = defineVaporComponent({
+        setup() {
+          const createForwardedSlot = forwardedSlotCreator()
+          const n2 = createComponent(Child, null, {
+            default: () => {
+              const n0 = createForwardedSlot('default', null, () => {
+                return template('<!-- <div></div> -->')()
+              })
+              return n0
+            },
+          })
+          return n2
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return createComponent(Parent, null, {
+            default: () => template('<!-- <div>App</div> -->')(),
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('child fallback<!--slot--><!--slot-->')
+    })
+
+    test('forwarded slot with fallback (v-if)', async () => {
+      const Child = defineVaporComponent({
+        setup() {
+          return createSlot('default', null, () => template('child fallback')())
+        },
+      })
+
+      const show = ref(false)
+      const Parent = defineVaporComponent({
+        setup() {
+          const createForwardedSlot = forwardedSlotCreator()
+          const n2 = createComponent(Child, null, {
+            default: () => {
+              const n0 = createForwardedSlot('default', null, () => {
+                const n2 = createIf(
+                  () => show.value,
+                  () => {
+                    const n4 = template('<div>if content</div>')()
+                    return n4
+                  },
+                )
+                return n2
+              })
+              return n0
+            },
+          })
+          return n2
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return createComponent(Parent, null, {
+            default: () => template('<!-- <div>App</div> -->')(),
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('child fallback<!--if--><!--slot--><!--slot-->')
+
+      show.value = true
+      await nextTick()
+      expect(html()).toBe(
+        '<div>if content</div><!--if--><!--slot--><!--slot-->',
+      )
+    })
+
+    test('forwarded slot with fallback (v-for)', async () => {
+      const Child = defineVaporComponent({
+        setup() {
+          return createSlot('default', null, () => template('child fallback')())
+        },
+      })
+
+      const items = ref<number[]>([])
+      const Parent = defineVaporComponent({
+        setup() {
+          const createForwardedSlot = forwardedSlotCreator()
+          const n2 = createComponent(Child, null, {
+            default: () => {
+              const n0 = createForwardedSlot('default', null, () => {
+                const n2 = createFor(
+                  () => items.value,
+                  for_item0 => {
+                    const n4 = template('<span> </span>')() as any
+                    const x4 = child(n4) as any
+                    renderEffect(() =>
+                      setText(x4, toDisplayString(for_item0.value)),
+                    )
+                    return n4
+                  },
+                )
+                return n2
+              })
+              return n0
+            },
+          })
+          return n2
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return createComponent(Parent, null, {
+            default: () => template('<!-- <div>App</div> -->')(),
+          })
+        },
+      }).render()
+
+      expect(html()).toBe('child fallback<!--for--><!--slot--><!--slot-->')
+
+      items.value.push(1)
+      await nextTick()
+      expect(html()).toBe('<span>1</span><!--for--><!--slot--><!--slot-->')
+
+      items.value.pop()
+      await nextTick()
+      expect(html()).toBe('child fallback<!--for--><!--slot--><!--slot-->')
+    })
+
+    describe('vdom interop', () => {
+      const createVaporSlot = (fallbackText = 'fallback') => {
+        return defineVaporComponent({
+          setup() {
+            const n0 = createSlot('foo', null, () => {
+              const n2 = template(`<div>${fallbackText}</div>`)()
+              return n2
+            })
+            return n0
+          },
+        })
+      }
+
+      const createVdomSlot = (fallbackText = 'fallback') => {
+        return {
+          render(this: any) {
+            return renderSlot(this.$slots, 'foo', {}, () => [
+              h('div', fallbackText),
+            ])
+          },
+        }
+      }
+
+      const createVaporForwardedSlot = (
+        targetComponent: any,
+        fallbackText?: string,
+      ) => {
+        return defineVaporComponent({
+          setup() {
+            const createForwardedSlot = forwardedSlotCreator()
+            const n2 = createComponent(
+              targetComponent,
+              null,
+              {
+                foo: () => {
+                  return fallbackText
+                    ? createForwardedSlot('foo', null, () => {
+                        const n2 = template(`<div>${fallbackText}</div>`)()
+                        return n2
+                      })
+                    : createForwardedSlot('foo', null)
+                },
+              },
+              true,
+            )
+            return n2
+          },
+        })
+      }
+
+      const createVdomForwardedSlot = (
+        targetComponent: any,
+        fallbackText?: string,
+      ) => {
+        return {
+          render(this: any) {
+            return h(targetComponent, null, {
+              foo: () => [
+                fallbackText
+                  ? renderSlot(this.$slots, 'foo', {}, () => [
+                      h('div', fallbackText),
+                    ])
+                  : renderSlot(this.$slots, 'foo'),
+              ],
+              _: 3 /* FORWARDED */,
+            })
+          },
+        }
+      }
+
+      const createMultipleVaporForwardedSlots = (
+        targetComponent: any,
+        count: number,
+      ) => {
+        let current = targetComponent
+        for (let i = 0; i < count; i++) {
+          current = createVaporForwardedSlot(current)
+        }
+        return current
+      }
+
+      const createMultipleVdomForwardedSlots = (
+        targetComponent: any,
+        count: number,
+      ) => {
+        let current = targetComponent
+        for (let i = 0; i < count; i++) {
+          current = createVdomForwardedSlot(current)
+        }
+        return current
+      }
+
+      const createTestApp = (
+        rootComponent: any,
+        foo: Ref<string>,
+        show: Ref<Boolean>,
+      ) => {
+        return {
+          setup() {
+            return () =>
+              h(
+                rootComponent,
+                null,
+                createSlots({ _: 2 /* DYNAMIC */ } as any, [
+                  show.value
+                    ? {
+                        name: 'foo',
+                        fn: () => [h('span', foo.value)],
+                        key: '0',
+                      }
+                    : undefined,
+                ]),
+              )
+          },
+        }
+      }
+
+      const createEmptyTestApp = (rootComponent: any) => {
+        return {
+          setup() {
+            return () => h(rootComponent)
+          },
+        }
+      }
+
+      test('vdom slot > vapor forwarded slot > vapor slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot)
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>fallback</div><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot(with fallback) > vapor slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
+          VaporSlot,
+          'forwarded fallback',
+        )
+        const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>forwarded fallback</div><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VdomSlot = createVdomSlot()
+        const VaporForwardedSlot = createVaporForwardedSlot(VdomSlot)
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVdomSlot()
+        const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
+          VdomSlot,
+          'forwarded fallback',
+        )
+        const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVaporSlot()
+        const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot)
+        const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot)
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVaporSlot()
+        const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot)
+        const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
+          VdomForwardedSlot,
+          'forwarded fallback',
+        )
+        const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVaporSlot()
+        const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
+          VaporSlot,
+          'vdom fallback',
+        )
+        const VaporForwardedSlot = createVaporForwardedSlot(
+          VdomForwardedSlotWithFallback,
+        )
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVaporSlot()
+        const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
+          VaporSlot,
+          'vdom fallback',
+        )
+        const VaporForwardedSlot = createVaporForwardedSlot(
+          VdomForwardedSlotWithFallback,
+        )
+        const App = createEmptyTestApp(VaporForwardedSlot)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVdomSlot()
+        const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot)
+        const VaporForwardedSlot = createVaporForwardedSlot(VdomForwardedSlot)
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVdomSlot()
+        const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot)
+        const VaporForwardedSlotWithFallback = createVaporForwardedSlot(
+          VdomForwardedSlot,
+          'vapor fallback',
+        )
+        const App = createTestApp(VaporForwardedSlotWithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVdomSlot()
+
+        const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
+          VdomSlot,
+          'vdom fallback',
+        )
+        const VaporForwardedSlot = createVaporForwardedSlot(
+          VdomForwardedSlotWithFallback,
+        )
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVdomSlot()
+        const VdomForwardedSlot = createVdomForwardedSlot(VdomSlot)
+        const VaporForwardedSlot = createMultipleVaporForwardedSlots(
+          VdomForwardedSlot,
+          3,
+        )
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>fallback</div><!--slot--><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot (multiple) > vdom forwarded slot(with fallback) > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VdomSlot = createVdomSlot()
+        const VdomForwardedSlotWithFallback = createVdomForwardedSlot(
+          VdomSlot,
+          'vdom fallback',
+        )
+        const VaporForwardedSlot = createMultipleVaporForwardedSlots(
+          VdomForwardedSlotWithFallback,
+          3,
+        )
+        const App = createTestApp(VaporForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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(
+          '<div>vdom fallback</div><!--slot--><!--slot-->',
+        )
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+      })
+
+      test('vdom slot > vdom forwarded slot > vapor slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VdomForwardedSlot = createVdomForwardedSlot(VaporSlot)
+        const App = createTestApp(VdomForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVaporSlot()
+        const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot)
+        const VdomForwardedSlot = createVdomForwardedSlot(VaporForwardedSlot)
+        const App = createTestApp(VdomForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>fallback</div><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+      })
+
+      test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VaporForwardedSlot = createVaporForwardedSlot(VaporSlot)
+        const VdomForwardedSlot = createMultipleVdomForwardedSlots(
+          VaporForwardedSlot,
+          3,
+        )
+        const App = createTestApp(VdomForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>fallback</div><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+      })
+
+      test('vdom slot > vdom forwarded slot (multiple) > vapor forwarded slot(with fallback) > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VaporForwardedSlot = createVaporForwardedSlot(
+          VaporSlot,
+          'vapor fallback',
+        )
+        const VdomForwardedSlot = createMultipleVdomForwardedSlots(
+          VaporForwardedSlot,
+          3,
+        )
+        const App = createTestApp(VdomForwardedSlot, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>vapor fallback</div><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot > vapor forwarded slot > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VdomSlot = createVdomSlot()
+        const VaporForwardedSlot1 = createMultipleVaporForwardedSlots(
+          VdomSlot,
+          2,
+        )
+        const App = createTestApp(VaporForwardedSlot1, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>fallback</div><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VdomSlot = createVdomSlot()
+        const VaporForwardedSlot2 = createVaporForwardedSlot(VdomSlot)
+        const VaporForwardedSlot1WithFallback = createVaporForwardedSlot(
+          VaporForwardedSlot2,
+          'vapor1 fallback',
+        )
+        const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>vapor1 fallback</div><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot > vapor forwarded slot(with fallback) > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VdomSlot = createVdomSlot()
+        const VaporForwardedSlot2WithFallback = createVaporForwardedSlot(
+          VdomSlot,
+          'vapor2 fallback',
+        )
+        const VaporForwardedSlot1 = createVaporForwardedSlot(
+          VaporForwardedSlot2WithFallback,
+        )
+        const App = createTestApp(VaporForwardedSlot1, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>vapor2 fallback</div><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot > vapor forwarded slot > vapor slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VaporForwardedSlot2 = createVaporForwardedSlot(VaporSlot)
+        const VaporForwardedSlot1 =
+          createVaporForwardedSlot(VaporForwardedSlot2)
+        const App = createTestApp(VaporForwardedSlot1, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>fallback</div><!--slot--><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vdom slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VdomSlot = createVdomSlot()
+        const VaporForwardedSlot2WithFallback = createVaporForwardedSlot(
+          VdomSlot,
+          'vapor2 fallback',
+        )
+        const VaporForwardedSlot1WithFallback = createVaporForwardedSlot(
+          VaporForwardedSlot2WithFallback,
+          'vapor1 fallback',
+        )
+        const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('<div>vapor1 fallback</div><!--slot-->')
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot-->')
+      })
+
+      test('vdom slot > vapor forwarded slot(with fallback) > vapor forwarded slot(with fallback) > vapor slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VaporForwardedSlot2WithFallback = createVaporForwardedSlot(
+          VaporSlot,
+          'vapor2 fallback',
+        )
+        const VaporForwardedSlot1WithFallback = createVaporForwardedSlot(
+          VaporForwardedSlot2WithFallback,
+          'vapor1 fallback',
+        )
+        const App = createTestApp(VaporForwardedSlot1WithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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(
+          '<div>vapor1 fallback</div><!--slot--><!--slot-->',
+        )
+
+        show.value = true
+        await nextTick()
+        expect(root.innerHTML).toBe('<span>bar</span><!--slot--><!--slot-->')
+      })
+
+      test('vdom slot > vdom forwarded slot(with fallback) > vdom forwarded slot(with fallback) > vapor slot', async () => {
+        const foo = ref('foo')
+        const show = ref(true)
+
+        const VaporSlot = createVaporSlot()
+        const VdomForwardedSlot2WithFallback = createVdomForwardedSlot(
+          VaporSlot,
+          'vdom2 fallback',
+        )
+        const VdomForwardedSlot1WithFallback = createVdomForwardedSlot(
+          VdomForwardedSlot2WithFallback,
+          'vdom1 fallback',
+        )
+        const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVdomSlot()
+        const VdomForwardedSlot2WithFallback = createVdomForwardedSlot(
+          VdomSlot,
+          'vdom2 fallback',
+        )
+        const VdomForwardedSlot1WithFallback = createVdomForwardedSlot(
+          VdomForwardedSlot2WithFallback,
+          'vdom1 fallback',
+        )
+        const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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 = createVaporSlot()
+        const VdomForwardedSlot3WithFallback = createVdomForwardedSlot(
+          VaporSlot,
+          'vdom3 fallback',
+        )
+        const VdomForwardedSlot2WithFallback = createVdomForwardedSlot(
+          VdomForwardedSlot3WithFallback,
+          'vdom2 fallback',
+        )
+        const VdomForwardedSlot1WithFallback = createVdomForwardedSlot(
+          VdomForwardedSlot2WithFallback,
+          'vdom1 fallback',
+        )
+        const App = createTestApp(VdomForwardedSlot1WithFallback, foo, show)
+
+        const root = document.createElement('div')
+        createApp(App).use(vaporInteropPlugin).mount(root)
+        expect(root.innerHTML).toBe('<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('consecutive slots with insertion state', async () => {
       const { component: Child } = define({
         setup() {
index 763bb1defed23dd7b90eb9b34a31da8ba69be440..71a448d2f7171e83af304bf15805566c050b4b1a 100644 (file)
@@ -15,8 +15,10 @@ import { isArray, isObject, isString } from '@vue/shared'
 import { createComment, createTextNode } from './dom/node'
 import {
   type Block,
+  ForFragment,
   VaporFragment,
   insert,
+  remove,
   remove as removeBlock,
 } from './block'
 import { warn } from '@vue/runtime-dom'
@@ -81,7 +83,7 @@ export const createFor = (
   setup?: (_: {
     createSelector: (source: () => any) => (cb: () => void) => void
   }) => void,
-): VaporFragment => {
+): ForFragment => {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
   if (isHydrating) {
@@ -98,7 +100,7 @@ export const createFor = (
   let currentKey: any
   // TODO handle this in hydration
   const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
-  const frag = new VaporFragment(oldBlocks)
+  const frag = new ForFragment(oldBlocks)
   const instance = currentInstance!
   const canUseFastRemove = !!(flags & VaporVForFlags.FAST_REMOVE)
   const isComponent = !!(flags & VaporVForFlags.IS_COMPONENT)
@@ -116,6 +118,7 @@ export const createFor = (
     const newLength = source.values.length
     const oldLength = oldBlocks.length
     newBlocks = new Array(newLength)
+    let isFallback = false
 
     const prevSub = setActiveSub()
 
@@ -127,6 +130,11 @@ export const createFor = (
     } else {
       parent = parent || parentAnchor!.parentNode
       if (!oldLength) {
+        // remove fallback nodes
+        if (frag.fallback && (frag.nodes[0] as Block[]).length > 0) {
+          remove(frag.nodes[0], parent!)
+        }
+
         // fast path for all new
         for (let i = 0; i < newLength; i++) {
           mount(source, i)
@@ -144,6 +152,12 @@ export const createFor = (
           parent!.textContent = ''
           parent!.appendChild(parentAnchor)
         }
+
+        // render fallback nodes
+        if (frag.fallback) {
+          insert((frag.nodes[0] = frag.fallback()), parent!, parentAnchor)
+          isFallback = true
+        }
       } else if (!getKey) {
         // unkeyed fast path
         const commonLength = Math.min(newLength, oldLength)
@@ -343,11 +357,12 @@ export const createFor = (
       }
     }
 
-    frag.nodes = [(oldBlocks = newBlocks)]
-    if (parentAnchor) {
-      frag.nodes.push(parentAnchor)
+    if (!isFallback) {
+      frag.nodes = [(oldBlocks = newBlocks)]
+      if (parentAnchor) frag.nodes.push(parentAnchor)
+    } else {
+      oldBlocks = []
     }
-
     setActiveSub(prevSub)
   }
 
index 2574287b72c15e94aec860e743486377f100d682..3aa5db41d7ce98f314f85fac8cbd2446112fd2c8 100644 (file)
@@ -47,12 +47,15 @@ export type Block = TransitionBlock | VaporComponentInstance | Block[]
 
 export type BlockFn = (...args: any[]) => Block
 
-export class VaporFragment implements TransitionOptions {
-  $key?: any
-  $transition?: VaporTransitionHooks | undefined
-  nodes: Block
+export class VaporFragment<T extends Block = Block>
+  implements TransitionOptions
+{
+  nodes: T
   vnode?: VNode | null = null
   anchor?: Node
+  fallback?: BlockFn
+  $key?: any
+  $transition?: VaporTransitionHooks | undefined
   insert?: (
     parent: ParentNode,
     anchor: Node | null,
@@ -60,16 +63,21 @@ export class VaporFragment implements TransitionOptions {
   ) => void
   remove?: (parent?: ParentNode, transitionHooks?: TransitionHooks) => void
 
-  constructor(nodes: Block) {
+  constructor(nodes: T) {
     this.nodes = nodes
   }
 }
 
+export class ForFragment extends VaporFragment<Block[]> {
+  constructor(nodes: Block[]) {
+    super(nodes)
+  }
+}
+
 export class DynamicFragment extends VaporFragment {
   anchor: Node
   scope: EffectScope | undefined
   current?: BlockFn
-  fallback?: BlockFn
 
   constructor(anchorLabel?: string) {
     super([])
@@ -125,18 +133,75 @@ export class DynamicFragment extends VaporFragment {
 
     renderBranch()
 
-    if (this.fallback && !isValidBlock(this.nodes)) {
-      parent && remove(this.nodes, parent)
-      this.nodes =
-        (this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
-        []
-      parent && insert(this.nodes, parent, this.anchor)
+    if (this.fallback) {
+      // set fallback for nested fragments
+      const hasNestedFragment = isFragment(this.nodes)
+      if (hasNestedFragment) {
+        setFragmentFallback(this.nodes as VaporFragment, this.fallback)
+      }
+
+      const invalidFragment = findInvalidFragment(this)
+      if (invalidFragment) {
+        parent && remove(this.nodes, parent)
+        const scope = this.scope || (this.scope = new EffectScope())
+        scope.run(() => {
+          // for nested fragments, render invalid fragment's fallback
+          if (hasNestedFragment) {
+            renderFragmentFallback(invalidFragment)
+          } else {
+            this.nodes = this.fallback!() || []
+          }
+        })
+        parent && insert(this.nodes, parent, this.anchor)
+      }
     }
 
     setActiveSub(prevSub)
   }
 }
 
+export function setFragmentFallback(
+  fragment: VaporFragment,
+  fallback: BlockFn,
+): void {
+  if (fragment.fallback) {
+    const originalFallback = fragment.fallback
+    // if the original fallback also renders invalid blocks,
+    // this ensures proper fallback chaining
+    fragment.fallback = () => {
+      const fallbackNodes = originalFallback()
+      if (isValidBlock(fallbackNodes)) {
+        return fallbackNodes
+      }
+      return fallback()
+    }
+  } else {
+    fragment.fallback = fallback
+  }
+
+  if (isFragment(fragment.nodes)) {
+    setFragmentFallback(fragment.nodes, fragment.fallback)
+  }
+}
+
+function renderFragmentFallback(fragment: VaporFragment): void {
+  if (fragment instanceof ForFragment) {
+    fragment.nodes[0] = [fragment.fallback!() || []] as Block[]
+  } else if (fragment instanceof DynamicFragment) {
+    fragment.update(fragment.fallback)
+  } else {
+    // vdom slots
+  }
+}
+
+function findInvalidFragment(fragment: VaporFragment): VaporFragment | null {
+  if (isValidBlock(fragment.nodes)) return null
+
+  return isFragment(fragment.nodes)
+    ? findInvalidFragment(fragment.nodes) || fragment
+    : fragment
+}
+
 export function isFragment(val: NonNullable<unknown>): val is VaporFragment {
   return val instanceof VaporFragment
 }
@@ -156,7 +221,7 @@ export function isValidBlock(block: Block): boolean {
   } else if (isVaporComponent(block)) {
     return isValidBlock(block.block)
   } else if (isArray(block)) {
-    return block.length > 0 && block.every(isValidBlock)
+    return block.length > 0 && block.some(isValidBlock)
   } else {
     // fragment
     return isValidBlock(block.nodes)
@@ -181,7 +246,7 @@ export function insert(
         performTransitionEnter(
           block,
           (block as TransitionBlock).$transition as TransitionHooks,
-          () => parent.insertBefore(block, anchor),
+          () => parent.insertBefore(block, anchor as Node),
           parentSuspense,
         )
       } else {
@@ -199,6 +264,10 @@ export function insert(
       insert(b, parent, anchor)
     }
   } else {
+    if (block.anchor) {
+      insert(block.anchor, parent, anchor)
+      anchor = block.anchor
+    }
     // fragment
     if (block.insert) {
       // TODO handle hydration for vdom interop
@@ -206,7 +275,6 @@ export function insert(
     } else {
       insert(block.nodes, parent, anchor, parentSuspense)
     }
-    if (block.anchor) insert(block.anchor, parent, anchor)
   }
 }
 
index 100c99cdb8af19f17437e323fbe19806c9769c97..78d823b367424ff1797865ef626b882cc04e79a2 100644 (file)
@@ -91,10 +91,21 @@ export function getSlot(
   }
 }
 
+export function forwardedSlotCreator(): (
+  name: string | (() => string),
+  rawProps?: LooseRawProps | null,
+  fallback?: VaporSlot,
+) => Block {
+  const instance = currentInstance as VaporComponentInstance
+  return (name, rawProps, fallback) =>
+    createSlot(name, rawProps, fallback, instance)
+}
+
 export function createSlot(
   name: string | (() => string),
   rawProps?: LooseRawProps | null,
   fallback?: VaporSlot,
+  i?: VaporComponentInstance,
 ): Block {
   const _insertionParent = insertionParent
   const _insertionAnchor = insertionAnchor
@@ -104,7 +115,7 @@ export function createSlot(
     resetInsertionState()
   }
 
-  const instance = currentInstance as VaporComponentInstance
+  const instance = i || (currentInstance as VaporComponentInstance)
   const rawSlots = instance.rawSlots
   const slotProps = rawProps
     ? new Proxy(rawProps, rawPropsProxyHandlers)
@@ -126,18 +137,10 @@ export function createSlot(
     const renderSlot = () => {
       const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
       if (slot) {
+        fragment.fallback = fallback
         // create and cache bound version of the slot to make it stable
         // so that we avoid unnecessary updates if it resolves to the same slot
-        fragment.update(
-          slot._bound ||
-            (slot._bound = () => {
-              const slotContent = slot(slotProps)
-              if (slotContent instanceof DynamicFragment) {
-                slotContent.fallback = fallback
-              }
-              return slotContent
-            }),
-        )
+        fragment.update(slot._bound || (slot._bound = () => slot(slotProps)))
       } else {
         fragment.update(fallback)
       }
index a6f78ca1d8504992c57ac8955962fb32dff27e43..6e8f4d9694031d35181ad017588b77fdcd90e9bf 100644 (file)
@@ -14,7 +14,7 @@ export {
   isVaporComponent,
 } from './component'
 export { renderEffect } from './renderEffect'
-export { createSlot } from './componentSlots'
+export { createSlot, forwardedSlotCreator } from './componentSlots'
 export { template } from './dom/template'
 export { createTextNode, child, nthChild, next } from './dom/node'
 export {
index edaec9c6ffb2c511feed13335746c8d5475fd8f4..98470c37ffa765aefa1834dd67dcb2343abcaf8f 100644 (file)
@@ -4,7 +4,9 @@ import {
   type ConcreteComponent,
   MoveType,
   type Plugin,
+  type RendererElement,
   type RendererInternals,
+  type RendererNode,
   type ShallowRef,
   type Slots,
   type TransitionHooks,
@@ -14,8 +16,10 @@ import {
   createVNode,
   currentInstance,
   ensureRenderer,
+  ensureVaporSlotFallback,
   isEmitListener,
   isKeepAlive,
+  isVNode,
   onScopeDispose,
   renderSlot,
   setTransitionHooks as setVNodeTransitionHooks,
@@ -39,12 +43,15 @@ import {
   VaporFragment,
   type VaporTransitionHooks,
   insert,
+  isFragment,
   remove,
+  setFragmentFallback,
 } from './block'
 import {
   EMPTY_OBJ,
   ShapeFlags,
   extend,
+  isArray,
   isFunction,
   isReservedProp,
 } from '@vue/shared'
@@ -143,15 +150,18 @@ const vaporInteropImpl: Omit<
   slot(n1: VNode, n2: VNode, container, anchor) {
     if (!n1) {
       // mount
-      const selfAnchor = (n2.el = n2.anchor = createTextNode())
-      insert(selfAnchor, container, anchor)
+      let selfAnchor: Node | undefined
       const { slot, fallback } = n2.vs!
       const propsRef = (n2.vs!.ref = shallowRef(n2.props))
       const slotBlock = slot(new Proxy(propsRef, vaporSlotPropsProxyHandler))
-      // TODO fallback for slot with v-if content
-      // fallback is a vnode slot function here, and slotBlock, if a DynamicFragment,
-      // expects a Vapor BlockFn as fallback
-      fallback
+      // handle nested fragments
+      if (fallback && isFragment(slotBlock)) {
+        setFragmentFallback(slotBlock, createFallback(fallback))
+        // use fragment's anchor when possible
+        selfAnchor = slotBlock.anchor
+      }
+      if (!selfAnchor) selfAnchor = createTextNode()
+      insert((n2.el = n2.anchor = selfAnchor), container, anchor)
       insert((n2.vb = slotBlock), container, selfAnchor)
     } else {
       // update
@@ -308,7 +318,7 @@ function createVDOMComponent(
       )
     }
 
-    frag.nodes = vnode.el as Block
+    frag.nodes = vnode.el as any
     simpleSetCurrentInstance(prev)
   }
 
@@ -334,34 +344,56 @@ function renderVDOMSlot(
   let fallbackNodes: Block | undefined
   let oldVNode: VNode | null = null
 
+  frag.fallback = fallback
   frag.insert = (parentNode, anchor) => {
     if (!isMounted) {
       renderEffect(() => {
-        const vnode = renderSlot(
-          slotsRef.value,
-          isFunction(name) ? name() : name,
-          props,
-        )
-        if ((vnode.children as any[]).length) {
+        let vnode: VNode | undefined
+        let isValidSlot = false
+        // only render slot if rawSlots is defined and slot nodes are not empty
+        // otherwise, render fallback
+        if (slotsRef.value) {
+          vnode = renderSlot(
+            slotsRef.value,
+            isFunction(name) ? name() : name,
+            props,
+          )
+
+          let children = vnode.children as any[]
+          // handle forwarded vapor slot without its own fallback
+          // use the fallback provided by the slot outlet
+          ensureVaporSlotFallback(children, fallback as any)
+          isValidSlot = children.length > 0
+        }
+
+        if (isValidSlot) {
           if (fallbackNodes) {
             remove(fallbackNodes, parentNode)
             fallbackNodes = undefined
           }
           internals.p(
             oldVNode,
-            vnode,
+            vnode!,
             parentNode,
             anchor,
             parentComponent as any,
           )
-          oldVNode = vnode
+          oldVNode = vnode!
         } else {
+          // for forwarded slot without its own fallback, use the fallback
+          // provided by the slot outlet.
+          // re-fetch `frag.fallback` as it may have been updated at `createSlot`
+          fallback = frag.fallback
           if (fallback && !fallbackNodes) {
             // mount fallback
             if (oldVNode) {
               internals.um(oldVNode, parentComponent as any, null, true)
             }
-            insert((fallbackNodes = fallback(props)), parentNode, anchor)
+            insert(
+              (fallbackNodes = fallback(internals, parentComponent)),
+              parentNode,
+              anchor,
+            )
           }
           oldVNode = null
         }
@@ -403,3 +435,31 @@ export const vaporInteropPlugin: Plugin = app => {
     return mount(...args)
   }) satisfies App['mount']
 }
+
+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
+  }