]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
test(vapor): componentSlots
authorEvan You <evan@vuejs.org>
Tue, 10 Dec 2024 13:36:06 +0000 (21:36 +0800)
committerEvan You <evan@vuejs.org>
Tue, 10 Dec 2024 13:36:06 +0000 (21:36 +0800)
packages/runtime-vapor/__tests__/componentProps.spec.ts
packages/runtime-vapor/__tests__/componentSlots.spec.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/component.ts
packages/runtime-vapor/src/componentProps.ts
packages/runtime-vapor/src/componentSlots.ts

index 6d87041f90c04cf18ab54577323fa06fdf930b92..3f8270b3d90669899a25d90423f862a2cb1f388d 100644 (file)
@@ -476,11 +476,6 @@ describe('component: props', () => {
     expect(changeSpy).toHaveBeenCalledTimes(1)
   })
 
-  // #3371
-  test.todo(`avoid double-setting props when casting`, async () => {
-    // TODO: provide, slots
-  })
-
   test('support null in required + multiple-type declarations', () => {
     const { render } = define({
       props: {
index 2cdf8fe7309f5ba94a8da8c813589062cc63dbda..03c96dbc1307425172c1550c6aa151ebdc96f5c6 100644 (file)
@@ -2,30 +2,29 @@
 
 import {
   createComponent,
+  // @ts-expect-error
   createForSlots,
   createSlot,
   createVaporApp,
-  defineComponent,
-  getCurrentInstance,
+  defineVaporComponent,
   insert,
-  nextTick,
   prepend,
-  ref,
   renderEffect,
   setText,
   template,
-  withDestructure,
 } from '../src'
+import { currentInstance, nextTick, ref } from '@vue/runtime-dom'
 import { makeRender } from './_utils'
 
 const define = makeRender<any>()
+
 function renderWithSlots(slots: any): any {
   let instance: any
-  const Comp = defineComponent({
-    render() {
+  const Comp = defineVaporComponent({
+    setup() {
       const t0 = template('<div></div>')
       const n0 = t0()
-      instance = getCurrentInstance()
+      instance = currentInstance
       return n0
     },
   })
@@ -40,51 +39,12 @@ function renderWithSlots(slots: any): any {
   return instance
 }
 
-describe.todo('component: slots', () => {
-  test('initSlots: instance.slots should be set correctly', () => {
-    let instance: any
-    const Comp = defineComponent({
-      render() {
-        const t0 = template('<div></div>')
-        const n0 = t0()
-        instance = getCurrentInstance()
-        return n0
-      },
-    })
-
-    const { render } = define({
-      render() {
-        return createComponent(Comp, {}, { header: () => template('header')() })
-      },
-    })
-
-    render()
-
-    expect(instance.slots.header()).toMatchObject(
-      document.createTextNode('header'),
-    )
-  })
-
-  // NOTE: slot normalization is not supported
-  test.todo(
-    'initSlots: should normalize object slots (when value is null, string, array)',
-    () => {},
-  )
-  test.todo(
-    'initSlots: should normalize object slots (when value is function)',
-    () => {},
-  )
-
-  // runtime-core's "initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
+describe('component: slots', () => {
   test('initSlots: instance.slots should be set correctly', () => {
     const { slots } = renderWithSlots({
       default: () => template('<span></span>')(),
     })
 
-    // expect(
-    //   '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
-    // ).toHaveBeenWarned()
-
     expect(slots.default()).toMatchObject(document.createElement('span'))
   })
 
@@ -93,223 +53,60 @@ describe.todo('component: slots', () => {
 
     let instance: any
     const Child = () => {
-      instance = getCurrentInstance()
+      instance = currentInstance
       return template('child')()
     }
 
     const { render } = define({
       render() {
-        return createComponent(Child, {}, [
-          () =>
-            flag1.value
-              ? { name: 'one', fn: () => template('<span></span>')() }
-              : { name: 'two', fn: () => template('<div></div>')() },
-        ])
-      },
-    })
-
-    render()
-
-    expect(instance.slots).toHaveProperty('one')
-    expect(instance.slots).not.toHaveProperty('two')
-
-    flag1.value = false
-    await nextTick()
-
-    expect(instance.slots).not.toHaveProperty('one')
-    expect(instance.slots).toHaveProperty('two')
-  })
-
-  // NOTE: it is not supported
-  // test('updateSlots: instance.slots should be updated correctly (when slotType is null)', () => {})
-
-  // runtime-core's "updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
-  test('updateSlots: instance.slots should be update correctly', async () => {
-    const flag1 = ref(true)
-
-    let instance: any
-    const Child = () => {
-      instance = getCurrentInstance()
-      return template('child')()
-    }
-
-    const { render } = define({
-      setup() {
-        return createComponent(Child, {}, [
-          () =>
-            flag1.value
-              ? { name: 'header', fn: () => template('header')() }
-              : { name: 'footer', fn: () => template('footer')() },
-        ])
-      },
-    })
-    render()
-
-    expect(instance.slots).toHaveProperty('header')
-    flag1.value = false
-    await nextTick()
-
-    // expect(
-    //   '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
-    // ).toHaveBeenWarned()
-
-    expect(instance.slots).toHaveProperty('footer')
-  })
-
-  test('the current instance should be kept in the slot', async () => {
-    let instanceInDefaultSlot: any
-    let instanceInFooSlot: any
-
-    const Comp = defineComponent({
-      render() {
-        const instance = getCurrentInstance()
-        instance!.slots.default!()
-        instance!.slots.foo!()
-        return template('<div></div>')()
-      },
-    })
-
-    const { instance } = define({
-      render() {
-        return createComponent(Comp, {}, [
-          {
-            default: () => {
-              instanceInDefaultSlot = getCurrentInstance()
-              return template('content')()
-            },
-            foo: () => {
-              instanceInFooSlot = getCurrentInstance()
-              return template('content')()
-            },
-          },
-        ])
-      },
-    }).render()
-
-    expect(instanceInDefaultSlot).toBe(instance)
-    expect(instanceInFooSlot).toBe(instance)
-  })
-
-  test('the current instance should be kept in the dynamic slots', async () => {
-    let instanceInDefaultSlot: any
-    let instanceInVForSlot: any
-    let instanceInVIfSlot: any
-
-    const Comp = defineComponent({
-      render() {
-        const instance = getCurrentInstance()
-        instance!.slots.default!()
-        instance!.slots.inVFor!()
-        instance!.slots.inVIf!()
-        return template('<div></div>')()
-      },
-    })
-
-    const { instance } = define({
-      render() {
-        return createComponent(Comp, {}, [
+        return createComponent(
+          Child,
+          {},
           {
-            default: () => {
-              instanceInDefaultSlot = getCurrentInstance()
-              return template('content')()
-            },
-          },
-          () => ({
-            name: 'inVFor',
-            fn: () => {
-              instanceInVForSlot = getCurrentInstance()
-              return template('content')()
-            },
-          }),
-          () => ({
-            name: 'inVIf',
-            fn: () => {
-              instanceInVIfSlot = getCurrentInstance()
-              return template('content')()
-            },
-          }),
-        ])
-      },
-    }).render()
-
-    expect(instanceInDefaultSlot).toBe(instance)
-    expect(instanceInVForSlot).toBe(instance)
-    expect(instanceInVIfSlot).toBe(instance)
-  })
-
-  test('dynamicSlots should update separately', async () => {
-    const flag1 = ref(true)
-    const flag2 = ref(true)
-    const slotFn1 = vitest.fn()
-    const slotFn2 = vitest.fn()
-
-    let instance: any
-    const Child = () => {
-      instance = getCurrentInstance()
-      return template('child')()
-    }
-
-    const { render } = define({
-      render() {
-        return createComponent(Child, {}, [
-          () => {
-            slotFn1()
-            return flag1.value
-              ? { name: 'one', fn: () => template('one')() }
-              : { name: 'two', fn: () => template('two')() }
+            $: [
+              () =>
+                flag1.value
+                  ? { name: 'one', fn: () => template('<span></span>')() }
+                  : { name: 'two', fn: () => template('<div></div>')() },
+            ],
           },
-          () => {
-            slotFn2()
-            return flag2.value
-              ? { name: 'three', fn: () => template('three')() }
-              : { name: 'four', fn: () => template('four')() }
-          },
-        ])
+        )
       },
     })
 
     render()
 
     expect(instance.slots).toHaveProperty('one')
-    expect(instance.slots).toHaveProperty('three')
-    expect(slotFn1).toHaveBeenCalledTimes(1)
-    expect(slotFn2).toHaveBeenCalledTimes(1)
+    expect(instance.slots).not.toHaveProperty('two')
 
     flag1.value = false
     await nextTick()
 
+    expect(instance.slots).not.toHaveProperty('one')
     expect(instance.slots).toHaveProperty('two')
-    expect(instance.slots).toHaveProperty('three')
-    expect(slotFn1).toHaveBeenCalledTimes(2)
-    expect(slotFn2).toHaveBeenCalledTimes(1)
-
-    flag2.value = false
-    await nextTick()
-
-    expect(instance.slots).toHaveProperty('two')
-    expect(instance.slots).toHaveProperty('four')
-    expect(slotFn1).toHaveBeenCalledTimes(2)
-    expect(slotFn2).toHaveBeenCalledTimes(2)
   })
 
-  test('should work with createFlorSlots', async () => {
+  test.todo('should work with createFlorSlots', async () => {
     const loop = ref([1, 2, 3])
 
     let instance: any
     const Child = () => {
-      instance = getCurrentInstance()
+      instance = currentInstance
       return template('child')()
     }
 
     const { render } = define({
       setup() {
-        return createComponent(Child, {}, [
-          () =>
-            createForSlots(loop.value, (item, i) => ({
-              name: item,
-              fn: () => template(item + i)(),
-            })),
-        ])
+        return createComponent(Child, null, {
+          $: [
+            () =>
+              // @ts-expect-error
+              createForSlots(loop.value, (item, i) => ({
+                name: item,
+                fn: () => template(item + i)(),
+              })),
+          ],
+        })
       },
     })
     render()
@@ -325,16 +122,11 @@ describe.todo('component: slots', () => {
     expect(instance.slots).not.toHaveProperty('1')
   })
 
-  test.todo('should respect $stable flag', async () => {
-    // TODO: $stable flag?
-  })
-
+  // passes but no warning for slot invocation in vapor currently
   test.todo('should not warn when mounting another app in setup', () => {
-    // TODO: warning
-    const Comp = defineComponent({
-      render() {
-        const i = getCurrentInstance()
-        return i!.slots.default!()
+    const Comp = defineVaporComponent({
+      setup(_, { slots }) {
+        return slots.default!()
       },
     })
     const mountComp = () => {
@@ -351,9 +143,7 @@ describe.todo('component: slots', () => {
     const App = {
       setup() {
         mountComp()
-      },
-      render() {
-        return null!
+        return []
       },
     }
     createVaporApp(App).mount(document.createElement('div'))
@@ -363,63 +153,58 @@ describe.todo('component: slots', () => {
   })
 
   describe('createSlot', () => {
-    test('slot should be render correctly', () => {
-      const Comp = defineComponent(() => {
+    test('slot should be rendered correctly', () => {
+      const Comp = defineVaporComponent(() => {
         const n0 = template('<div>')()
         insert(createSlot('header'), n0 as any as ParentNode)
         return n0
       })
 
       const { host } = define(() => {
-        return createComponent(Comp, {}, { header: () => template('header')() })
+        return createComponent(Comp, null, {
+          header: () => template('header')(),
+        })
       }).render()
 
-      expect(host.innerHTML).toBe('<div>header</div>')
+      expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
     })
 
-    test('slot should be render correctly with binds', async () => {
-      const Comp = defineComponent(() => {
+    test('slot should be rendered correctly with slot props', async () => {
+      const Comp = defineVaporComponent(() => {
         const n0 = template('<div></div>')()
         insert(
-          createSlot('header', [{ title: () => 'header' }]),
+          createSlot('header', { title: () => 'header' }),
           n0 as any as ParentNode,
         )
         return n0
       })
 
       const { host } = define(() => {
-        return createComponent(Comp, {}, [
-          {
-            header: withDestructure(
-              ({ title }) => [title],
-              ctx => {
-                const el = template('<h1></h1>')()
-                renderEffect(() => {
-                  setText(el, ctx[0])
-                })
-                return el
-              },
-            ),
+        return createComponent(Comp, null, {
+          header: props => {
+            const el = template('<h1></h1>')()
+            renderEffect(() => {
+              setText(el, props.title)
+            })
+            return el
           },
-        ])
+        })
       }).render()
 
-      expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
+      expect(host.innerHTML).toBe('<div><h1>header</h1><!--slot--></div>')
     })
 
     test('dynamic slot props', async () => {
       let props: any
 
       const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
-      const Comp = defineComponent(() =>
-        createSlot('default', [() => bindObj.value]),
+      const Comp = defineVaporComponent(() =>
+        createSlot('default', { $: [() => bindObj.value] }),
       )
       define(() =>
-        createComponent(
-          Comp,
-          {},
-          { default: _props => ((props = _props), []) },
-        ),
+        createComponent(Comp, null, {
+          default: _props => ((props = _props), []),
+        }),
       ).render()
 
       expect(props).toEqual({ foo: 1, baz: 'qux' })
@@ -438,15 +223,16 @@ describe.todo('component: slots', () => {
 
       const foo = ref(0)
       const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
-      const Comp = defineComponent(() =>
-        createSlot('default', [{ foo: () => foo.value }, () => bindObj.value]),
+      const Comp = defineVaporComponent(() =>
+        createSlot('default', {
+          foo: () => foo.value,
+          $: [() => bindObj.value],
+        }),
       )
       define(() =>
-        createComponent(
-          Comp,
-          {},
-          { default: _props => ((props = _props), []) },
-        ),
+        createComponent(Comp, null, {
+          default: _props => ((props = _props), []),
+        }),
       ).render()
 
       expect(props).toEqual({ foo: 100, baz: 'qux' })
@@ -460,123 +246,67 @@ describe.todo('component: slots', () => {
       expect(props).toEqual({ foo: 2, baz: 'qux' })
     })
 
-    test('slot class binding should be merged', async () => {
-      let props: any
-
-      const className = ref('foo')
-      const classObj = ref({ bar: true })
-      const Comp = defineComponent(() =>
-        createSlot('default', [
-          { class: () => className.value },
-          () => ({ class: ['baz', 'qux'] }),
-          { class: () => classObj.value },
-        ]),
-      )
-      define(() =>
-        createComponent(
-          Comp,
-          {},
-          { default: _props => ((props = _props), []) },
-        ),
-      ).render()
-
-      expect(props).toEqual({ class: 'foo baz qux bar' })
-
-      classObj.value.bar = false
-      await nextTick()
-      expect(props).toEqual({ class: 'foo baz qux' })
+    test('dynamic slot should be rendered correctly with slot props', async () => {
+      const val = ref('header')
 
-      className.value = ''
-      await nextTick()
-      expect(props).toEqual({ class: 'baz qux' })
-    })
-
-    test('slot style binding should be merged', async () => {
-      let props: any
-
-      const style = ref<any>({ fontSize: '12px' })
-      const Comp = defineComponent(() =>
-        createSlot('default', [
-          { style: () => style.value },
-          () => ({ style: { width: '100px', color: 'blue' } }),
-          { style: () => 'color: red' },
-        ]),
-      )
-      define(() =>
-        createComponent(
-          Comp,
-          {},
-          { default: _props => ((props = _props), []) },
-        ),
-      ).render()
-
-      expect(props).toEqual({
-        style: {
-          fontSize: '12px',
-          width: '100px',
-          color: 'red',
-        },
-      })
-
-      style.value = null
-      await nextTick()
-      expect(props).toEqual({
-        style: {
-          width: '100px',
-          color: 'red',
-        },
-      })
-    })
-
-    test('dynamic slot should be render correctly with binds', async () => {
-      const Comp = defineComponent(() => {
+      const Comp = defineVaporComponent(() => {
         const n0 = template('<div></div>')()
         prepend(
           n0 as any as ParentNode,
-          createSlot('header', [{ title: () => 'header' }]),
+          createSlot('header', { title: () => val.value }),
         )
         return n0
       })
 
       const { host } = define(() => {
         // dynamic slot
-        return createComponent(Comp, {}, [
-          () => ({
-            name: 'header',
-            fn: (props: any) => template(props.title)(),
-          }),
-        ])
+        return createComponent(Comp, null, {
+          $: [
+            () => ({
+              name: 'header',
+              fn: (props: any) => template(props.title)(),
+            }),
+          ],
+        })
       }).render()
 
       expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
+
+      val.value = 'footer'
+      await nextTick()
+      expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
     })
 
-    test('dynamic slot outlet should be render correctly with binds', async () => {
-      const Comp = defineComponent(() => {
+    test('dynamic slot outlet should be render correctly with slot props', async () => {
+      const val = ref('header')
+
+      const Comp = defineVaporComponent(() => {
         const n0 = template('<div></div>')()
         prepend(
           n0 as any as ParentNode,
           createSlot(
-            () => 'header', // dynamic slot outlet name
-            [{ title: () => 'header' }],
+            () => val.value, // dynamic slot outlet name
           ),
         )
         return n0
       })
 
       const { host } = define(() => {
-        return createComponent(
-          Comp,
-          {},
-          { header: props => template(props.title)() },
-        )
+        return createComponent(Comp, null, {
+          header: () => template('header')(),
+          footer: () => template('footer')(),
+        })
       }).render()
 
       expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
+
+      val.value = 'footer'
+      await nextTick()
+      expect(host.innerHTML).toBe('<div>footer<!--slot--></div>')
     })
 
     test('fallback should be render correctly', () => {
-      const Comp = defineComponent(() => {
+      const Comp = defineVaporComponent(() => {
         const n0 = template('<div></div>')()
         insert(
           createSlot('header', undefined, () => template('fallback')()),
@@ -595,24 +325,26 @@ describe.todo('component: slots', () => {
     test('dynamic slot should be updated correctly', async () => {
       const flag1 = ref(true)
 
-      const Child = defineComponent(() => {
+      const Child = defineVaporComponent(() => {
         const temp0 = template('<p></p>')
         const el0 = temp0()
         const el1 = temp0()
-        const slot1 = createSlot('one', [], () => template('one fallback')())
-        const slot2 = createSlot('two', [], () => template('two fallback')())
+        const slot1 = createSlot('one', null, () => template('one fallback')())
+        const slot2 = createSlot('two', null, () => template('two fallback')())
         insert(slot1, el0 as any as ParentNode)
         insert(slot2, el1 as any as ParentNode)
         return [el0, el1]
       })
 
       const { host } = define(() => {
-        return createComponent(Child, {}, [
-          () =>
-            flag1.value
-              ? { name: 'one', fn: () => template('one content')() }
-              : { name: 'two', fn: () => template('two content')() },
-        ])
+        return createComponent(Child, null, {
+          $: [
+            () =>
+              flag1.value
+                ? { name: 'one', fn: () => template('one content')() }
+                : { name: 'two', fn: () => template('two content')() },
+          ],
+        })
       }).render()
 
       expect(host.innerHTML).toBe(
@@ -637,7 +369,7 @@ describe.todo('component: slots', () => {
     test('dynamic slot outlet should be updated correctly', async () => {
       const slotOutletName = ref('one')
 
-      const Child = defineComponent(() => {
+      const Child = defineVaporComponent(() => {
         const temp0 = template('<p>')
         const el0 = temp0()
         const slot1 = createSlot(
@@ -674,7 +406,7 @@ describe.todo('component: slots', () => {
     })
 
     test('non-exist slot', async () => {
-      const Child = defineComponent(() => {
+      const Child = defineVaporComponent(() => {
         const el0 = template('<p>')()
         const slot = createSlot('not-exist', undefined)
         insert(slot, el0 as any as ParentNode)
@@ -685,7 +417,7 @@ describe.todo('component: slots', () => {
         return createComponent(Child)
       }).render()
 
-      expect(host.innerHTML).toBe('<p></p>')
+      expect(host.innerHTML).toBe('<p><!--slot--></p>')
     })
   })
 })
index 01cc30ceb576d0cd0401aed7d2604a214d4b94b9..3951be239b23c7d2104fcbadeeb63979a8cc67d8 100644 (file)
@@ -8,7 +8,12 @@ import {
 import { createComment } from './dom/node'
 import { EffectScope } from '@vue/reactivity'
 
-export type Block = Node | Fragment | VaporComponentInstance | Block[]
+export type Block =
+  | Node
+  | Fragment
+  | DynamicFragment
+  | VaporComponentInstance
+  | Block[]
 
 export type BlockFn = (...args: any[]) => Block
 
@@ -45,13 +50,12 @@ export class DynamicFragment extends Fragment {
     if (this.scope) {
       this.scope.stop()
       parent && remove(this.nodes, parent)
-      // TODO lifecycle unmount
     }
 
     if (render) {
       this.scope = new EffectScope()
       this.nodes = this.scope.run(render) || []
-      if (parent) insert(this.nodes, parent)
+      if (parent) insert(this.nodes, parent, this.anchor)
     } else {
       this.scope = undefined
       this.nodes = []
@@ -99,10 +103,11 @@ export function isValidBlock(block: Block): boolean {
 export function insert(
   block: Block,
   parent: ParentNode,
-  anchor: Node | null | 0 = null,
+  anchor: Node | null | 0 = null, // 0 means prepend
 ): void {
+  anchor = anchor === 0 ? parent.firstChild : anchor
   if (block instanceof Node) {
-    parent.insertBefore(block, anchor === 0 ? parent.firstChild : anchor)
+    parent.insertBefore(block, anchor)
   } else if (isVaporComponent(block)) {
     mountComponent(block, parent, anchor)
   } else if (isArray(block)) {
@@ -134,5 +139,8 @@ export function remove(block: Block, parent: ParentNode): void {
     // fragment
     remove(block.nodes, parent)
     if (block.anchor) remove(block.anchor, parent)
+    if ((block as DynamicFragment).scope) {
+      ;(block as DynamicFragment).scope!.stop()
+    }
   }
 }
index 2e0a98171670603c2ac906412e40a428117b2d9a..7c99de69947e9505926fcecd9dfddd9a22b4b09e 100644 (file)
@@ -113,7 +113,10 @@ interface SharedInternalOptions {
 // 100% strict. Here we use intentionally wider types to make `createComponent`
 // more ergonomic in tests and internal call sites, where we immediately cast
 // them into the stricter types.
-type LooseRawProps = Record<string, (() => unknown) | DynamicPropsSource[]> & {
+export type LooseRawProps = Record<
+  string,
+  (() => unknown) | DynamicPropsSource[]
+> & {
   $?: DynamicPropsSource[]
 }
 
index 1e615b578700e81a2065d5db53042d0a68dd9fef..2876a4e0d76f1c721cd89129a42c63a6d8cf97ed 100644 (file)
@@ -110,6 +110,7 @@ export function getPropsProxyHandlers(
     ? ({
         get: (target, key) => getProp(target, key),
         has: (_, key) => isProp(key),
+        ownKeys: () => Object.keys(propsOptions),
         getOwnPropertyDescriptor(target, key) {
           if (isProp(key)) {
             return {
@@ -119,7 +120,6 @@ export function getPropsProxyHandlers(
             }
           }
         },
-        ownKeys: () => Object.keys(propsOptions),
       } satisfies ProxyHandler<VaporComponentInstance>)
     : null
 
@@ -147,6 +147,7 @@ export function getPropsProxyHandlers(
   const attrsHandlers = {
     get: (target, key: string) => getAttr(target.rawProps, key),
     has: (target, key: string) => hasAttr(target.rawProps, key),
+    ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
     getOwnPropertyDescriptor(target, key: string) {
       if (hasAttr(target.rawProps, key)) {
         return {
@@ -156,25 +157,6 @@ export function getPropsProxyHandlers(
         }
       }
     },
-    ownKeys(target) {
-      const rawProps = target.rawProps
-      const keys: string[] = []
-      for (const key in rawProps) {
-        if (isAttr(key)) keys.push(key)
-      }
-      const dynamicSources = rawProps.$
-      if (dynamicSources) {
-        let i = dynamicSources.length
-        let source
-        while (i--) {
-          source = resolveSource(dynamicSources[i])
-          for (const key in source) {
-            if (isAttr(key)) keys.push(key)
-          }
-        }
-      }
-      return Array.from(new Set(keys))
-    },
   } satisfies ProxyHandler<VaporComponentInstance>
 
   if (__DEV__) {
@@ -221,6 +203,25 @@ export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
   return hasOwn(rawProps, key)
 }
 
+export function getKeysFromRawProps(rawProps: RawProps): string[] {
+  const keys: string[] = []
+  for (const key in rawProps) {
+    if (key !== '$') keys.push(key)
+  }
+  const dynamicSources = rawProps.$
+  if (dynamicSources) {
+    let i = dynamicSources.length
+    let source
+    while (i--) {
+      source = resolveSource(dynamicSources[i])
+      for (const key in source) {
+        keys.push(key)
+      }
+    }
+  }
+  return Array.from(new Set(keys))
+}
+
 export function normalizePropsOptions(
   comp: VaporComponent,
 ): NormalizedPropsOptions {
index 4ee38284026ffc128a1695109806f34ada182c83..1875651459e6f2450d2575d4c3d637149348153f 100644 (file)
@@ -3,10 +3,11 @@ import { type Block, type BlockFn, DynamicFragment } from './block'
 import {
   type RawProps,
   getAttrFromRawProps,
+  getKeysFromRawProps,
   hasAttrFromRawProps,
 } from './componentProps'
 import { currentInstance } from '@vue/runtime-core'
-import type { VaporComponentInstance } from './component'
+import type { LooseRawProps, VaporComponentInstance } from './component'
 import { renderEffect } from './renderEffect'
 
 export type RawSlots = Record<string, Slot> & {
@@ -86,7 +87,16 @@ export function getSlot(target: RawSlots, key: string): Slot | undefined {
 const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
   get: getAttrFromRawProps,
   has: hasAttrFromRawProps,
-  ownKeys: target => Object.keys(target).filter(k => k !== '$'),
+  ownKeys: getKeysFromRawProps,
+  getOwnPropertyDescriptor(target, key: string) {
+    if (hasAttrFromRawProps(target, key)) {
+      return {
+        configurable: true,
+        enumerable: true,
+        get: () => getAttrFromRawProps(target, key),
+      }
+    }
+  },
 }
 
 // TODO how to handle empty slot return blocks?
@@ -95,11 +105,11 @@ const dynamicSlotsPropsProxyHandlers: ProxyHandler<RawProps> = {
 // and make the v-if use it as fallback
 export function createSlot(
   name: string | (() => string),
-  rawProps?: RawProps,
+  rawProps?: LooseRawProps | null,
   fallback?: Slot,
 ): Block {
+  const instance = currentInstance as VaporComponentInstance
   const fragment = new DynamicFragment('slot')
-  const rawSlots = (currentInstance as VaporComponentInstance)!.rawSlots
   const slotProps = rawProps
     ? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers)
     : EMPTY_OBJ
@@ -107,7 +117,7 @@ export function createSlot(
   // always create effect because a slot may contain dynamic root inside
   // which affects fallback
   renderEffect(() => {
-    const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
+    const slot = getSlot(instance.rawSlots, isFunction(name) ? name() : name)
     if (slot) {
       fragment.update(
         () => slot(slotProps) || (fallback && fallback()),