فهرست منبع

feat(runtime-vapor): slot props (#227)

Rizumu Ayaka 1 سال پیش
والد
کامیت
c9241da4fa

+ 141 - 16
packages/runtime-vapor/__tests__/componentSlots.spec.ts

@@ -13,6 +13,7 @@ import {
   renderEffect,
   setText,
   template,
+  withDestructure,
 } from '../src'
 import { makeRender } from './_utils'
 
@@ -319,7 +320,7 @@ describe('component: slots', () => {
       const Comp = defineComponent(() => {
         const n0 = template('<div></div>')()
         insert(
-          createSlot('header', { title: () => 'header' }),
+          createSlot('header', [{ title: () => 'header' }]),
           n0 as any as ParentNode,
         )
         return n0
@@ -330,13 +331,16 @@ describe('component: slots', () => {
           Comp,
           {},
           {
-            header: ({ title }) => {
-              const el = template('<h1></h1>')()
-              renderEffect(() => {
-                setText(el, title())
-              })
-              return el
-            },
+            header: withDestructure(
+              ({ title }) => [title],
+              ctx => {
+                const el = template('<h1></h1>')()
+                renderEffect(() => {
+                  setText(el, ctx[0])
+                })
+                return el
+              },
+            ),
           },
         )
       }).render()
@@ -344,12 +348,133 @@ describe('component: slots', () => {
       expect(host.innerHTML).toBe('<div><h1>header</h1></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]),
+      )
+      define(() =>
+        createComponent(
+          Comp,
+          {},
+          { default: _props => ((props = _props), []) },
+        ),
+      ).render()
+
+      expect(props).toEqual({ foo: 1, baz: 'qux' })
+
+      bindObj.value.foo = 2
+      await nextTick()
+      expect(props).toEqual({ foo: 2, baz: 'qux' })
+
+      delete bindObj.value.baz
+      await nextTick()
+      expect(props).toEqual({ foo: 2 })
+    })
+
+    test('dynamic slot props with static slot props', async () => {
+      let props: any
+
+      const foo = ref(0)
+      const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
+      const Comp = defineComponent(() =>
+        createSlot('default', [{ foo: () => foo.value }, () => bindObj.value]),
+      )
+      define(() =>
+        createComponent(
+          Comp,
+          {},
+          { default: _props => ((props = _props), []) },
+        ),
+      ).render()
+
+      expect(props).toEqual({ foo: 100, baz: 'qux' })
+
+      foo.value = 2
+      await nextTick()
+      expect(props).toEqual({ foo: 100, baz: 'qux' })
+
+      delete bindObj.value.foo
+      await nextTick()
+      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' })
+
+      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 n0 = template('<div></div>')()
         prepend(
           n0 as any as ParentNode,
-          createSlot('header', { title: () => 'header' }),
+          createSlot('header', [{ title: () => 'header' }]),
         )
         return n0
       })
@@ -359,7 +484,7 @@ describe('component: slots', () => {
         return createComponent(Comp, {}, {}, [
           () => ({
             name: 'header',
-            fn: ({ title }) => template(`${title()}`)(),
+            fn: props => template(props.title)(),
           }),
         ])
       }).render()
@@ -374,7 +499,7 @@ describe('component: slots', () => {
           n0 as any as ParentNode,
           createSlot(
             () => 'header', // dynamic slot outlet name
-            { title: () => 'header' },
+            [{ title: () => 'header' }],
           ),
         )
         return n0
@@ -384,7 +509,7 @@ describe('component: slots', () => {
         return createComponent(
           Comp,
           {},
-          { header: ({ title }) => template(`${title()}`)() },
+          { header: props => template(props.title)() },
         )
       }).render()
 
@@ -395,7 +520,7 @@ describe('component: slots', () => {
       const Comp = defineComponent(() => {
         const n0 = template('<div></div>')()
         insert(
-          createSlot('header', {}, () => template('fallback')()),
+          createSlot('header', undefined, () => template('fallback')()),
           n0 as any as ParentNode,
         )
         return n0
@@ -415,8 +540,8 @@ describe('component: slots', () => {
         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', [], () => template('one fallback')())
+        const slot2 = createSlot('two', [], () => template('two fallback')())
         insert(slot1, el0 as any as ParentNode)
         insert(slot2, el1 as any as ParentNode)
         return [el0, el1]
@@ -458,7 +583,7 @@ describe('component: slots', () => {
         const el0 = temp0()
         const slot1 = createSlot(
           () => slotOutletName.value,
-          {},
+          undefined,
           () => template('fallback')(),
         )
         insert(slot1, el0 as any as ParentNode)

+ 64 - 3
packages/runtime-vapor/src/componentSlots.ts

@@ -14,6 +14,9 @@ import { type Block, type Fragment, fragmentKey } from './apiRender'
 import { firstEffect, renderEffect } from './renderEffect'
 import { createComment, createTextNode, insert, remove } from './dom/element'
 import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
+import type { NormalizedRawProps } from './componentProps'
+import type { Data } from '@vue/runtime-shared'
+import { mergeProps } from './dom/prop'
 
 // TODO: SSR
 
@@ -106,7 +109,7 @@ export function initSlots(
 
 export function createSlot(
   name: string | (() => string),
-  binds?: Record<string, (() => unknown) | undefined>,
+  binds?: NormalizedRawProps,
   fallback?: () => Block,
 ): Block {
   let block: Block | undefined
@@ -120,7 +123,7 @@ export function createSlot(
 
   // When not using dynamic slots, simplify the process to improve performance
   if (!isDynamicName && !isReactive(slots)) {
-    if ((branch = slots[name] || fallback)) {
+    if ((branch = withProps(slots[name]) || fallback)) {
       return branch(binds)
     } else {
       return []
@@ -137,7 +140,7 @@ export function createSlot(
 
   // TODO lifecycle hooks
   renderEffect(() => {
-    if ((branch = getSlot() || fallback) !== oldBranch) {
+    if ((branch = withProps(getSlot()) || fallback) !== oldBranch) {
       parent ||= anchor.parentNode
       if (block) {
         scope!.stop()
@@ -155,4 +158,62 @@ export function createSlot(
   })
 
   return fragment
+
+  function withProps<T extends (p: any) => any>(fn?: T) {
+    if (fn)
+      return (binds?: NormalizedRawProps): ReturnType<T> =>
+        fn(binds && normalizeSlotProps(binds))
+  }
+}
+
+function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
+  const { length } = rawPropsList
+  const mergings = length > 1 ? shallowReactive<Data[]>([]) : undefined
+  const result = shallowReactive<Data>({})
+
+  for (let i = 0; i < length; i++) {
+    const rawProps = rawPropsList[i]
+    if (isFunction(rawProps)) {
+      // dynamic props
+      renderEffect(() => {
+        const props = rawProps()
+        if (mergings) {
+          mergings[i] = props
+        } else {
+          setDynamicProps(props)
+        }
+      })
+    } else {
+      // static props
+      const props = mergings
+        ? (mergings[i] = shallowReactive<Data>({}))
+        : result
+      for (const key in rawProps) {
+        const valueSource = rawProps[key]
+        renderEffect(() => {
+          props[key] = valueSource()
+        })
+      }
+    }
+  }
+
+  if (mergings) {
+    renderEffect(() => {
+      setDynamicProps(mergeProps(...mergings))
+    })
+  }
+
+  return result
+
+  function setDynamicProps(props: Data) {
+    const otherExistingKeys = new Set(Object.keys(result))
+    for (const key in props) {
+      result[key] = props[key]
+      otherExistingKeys.delete(key)
+    }
+    // delete other stale props
+    for (const key of otherExistingKeys) {
+      delete result[key]
+    }
+  }
 }

+ 1 - 1
packages/runtime-vapor/src/dom/prop.ts

@@ -154,7 +154,7 @@ export function setDynamicProps(el: Element, ...args: any) {
 }
 
 // TODO copied from runtime-core
-function mergeProps(...args: Data[]) {
+export function mergeProps(...args: Data[]) {
   const ret: Data = {}
   for (let i = 0; i < args.length; i++) {
     const toMerge = args[i]