Ver Fonte

feat(runtime-vapor): allow VDOM components to directly invoke vapor slots via `slots.name()` (#14273)

edison há 3 meses atrás
pai
commit
6ffd55aba2

+ 6 - 2
packages/runtime-core/src/helpers/renderSlot.ts

@@ -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
   }
 

+ 158 - 0
packages/runtime-vapor/__tests__/vdomInterop.spec.ts

@@ -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', () => {

+ 7 - 0
packages/runtime-vapor/src/vdomInterop.ts

@@ -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
   },