]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(runtime-vapor): allow VDOM components to directly invoke vapor slots via `slots...
authoredison <daiwei521@126.com>
Mon, 5 Jan 2026 03:34:28 +0000 (11:34 +0800)
committerGitHub <noreply@github.com>
Mon, 5 Jan 2026 03:34:28 +0000 (11:34 +0800)
packages/runtime-core/src/helpers/renderSlot.ts
packages/runtime-vapor/__tests__/vdomInterop.spec.ts
packages/runtime-vapor/src/vdomInterop.ts

index 3a9389a5927e717646787df93762b83ef701fed7..247b06a79ab9f7958e2ded97ded2f863e6654297 100644 (file)
@@ -35,9 +35,13 @@ export function renderSlot(
   let slot = slots[name]
 
   // vapor slots rendered in vdom
-  if (slot && (slot as any).__vapor) {
+  // __vs: original vapor slot stored on a wrapper from vaporSlotsProxyHandler
+  // __vapor: marker indicating the slot itself is an original vapor slot
+  const vaporSlot =
+    slot && ((slot as any).__vs || ((slot as any).__vapor ? slot : null))
+  if (vaporSlot) {
     const ret = (openBlock(), createBlock(VaporSlot, props))
-    ret.vs = { slot, fallback }
+    ret.vs = { slot: vaporSlot, fallback }
     return ret
   }
 
index e3b7bfff64e80a6b3afc2ea7a6bac1df69f2b7b8..e348ab708f6401e570b8ee5342062673fd98ff39 100644 (file)
@@ -238,6 +238,164 @@ describe('vdomInterop', () => {
 
       expect(html()).toBe('default slot')
     })
+
+    test('slots.default() direct invocation', () => {
+      const VDomChild = defineComponent({
+        setup(_, { slots }) {
+          return () => h('div', null, slots.default!())
+        },
+      })
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            null,
+            {
+              default: () => template('direct call slot')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('<div>direct call slot</div>')
+    })
+
+    test('slots.default() with slot props', () => {
+      const VDomChild = defineComponent({
+        setup(_, { slots }) {
+          return () => h('div', null, slots.default!({ msg: 'hello' }))
+        },
+      })
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            null,
+            {
+              default: (props: { msg: string }) => {
+                const n0 = template('<span></span>')()
+                n0.textContent = props.msg
+                return [n0]
+              },
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('<div><span>hello</span></div>')
+    })
+
+    test('named slot with slots[name]() invocation', () => {
+      const VDomChild = defineComponent({
+        setup(_, { slots }) {
+          return () =>
+            h('div', null, [
+              h('header', null, slots.header!()),
+              h('main', null, slots.default!()),
+              h('footer', null, slots.footer!()),
+            ])
+        },
+      })
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            null,
+            {
+              header: () => template('Header')(),
+              default: () => template('Main')(),
+              footer: () => template('Footer')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe(
+        '<div><header>Header</header><main>Main</main><footer>Footer</footer></div>',
+      )
+    })
+
+    test('slots.default() return directly', () => {
+      const VDomChild = defineComponent({
+        setup(_, { slots }) {
+          return () => slots.default!()
+        },
+      })
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            null,
+            {
+              default: () => template('direct return slot')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('direct return slot')
+    })
+
+    test('rendering forwarding vapor slot', () => {
+      const VDomChild = defineComponent({
+        setup(_, { slots }) {
+          return () => h('div', null, { default: slots.default })
+        },
+      })
+
+      const VaporChild = defineVaporComponent({
+        setup() {
+          return createComponent(
+            VDomChild as any,
+            null,
+            {
+              default: () => template('forwarded slot')(),
+            },
+            true,
+          )
+        },
+      })
+
+      const { html } = define({
+        setup() {
+          return () => h(VaporChild as any)
+        },
+      }).render()
+
+      expect(html()).toBe('<div>forwarded slot</div>')
+    })
   })
 
   describe('provide / inject', () => {
index d07d74f191a81fa28b7320f4cae4413995bb0076..bb0619d10e323a9c024bd76ce784817f76c4f6aa 100644 (file)
@@ -280,6 +280,13 @@ const vaporSlotsProxyHandler: ProxyHandler<any> = {
     const slot = target[key]
     if (isFunction(slot)) {
       slot.__vapor = true
+      // Create a wrapper that internally uses renderSlot for proper vapor slot handling
+      // This ensures that calling slots.default() works the same as renderSlot(slots, 'default')
+      const wrapped = (props?: Record<string, any>) => [
+        renderSlot({ [key]: slot }, key as string, props),
+      ]
+      ;(wrapped as any).__vs = slot
+      return wrapped
     }
     return slot
   },