diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
index 5f115d8cc..56c8ac86c 100644
--- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts
+++ b/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('
')()
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('')()
- renderEffect(() => {
- setText(el, title())
- })
- return el
- },
+ header: withDestructure(
+ ({ title }) => [title],
+ ctx => {
+ const el = template('')()
+ renderEffect(() => {
+ setText(el, ctx[0])
+ })
+ return el
+ },
+ ),
},
)
}).render()
@@ -344,12 +348,133 @@ describe('component: slots', () => {
expect(host.innerHTML).toBe('header
')
})
+ test('dynamic slot props', async () => {
+ let props: any
+
+ const bindObj = ref>({ 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>({ 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({ 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('')()
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('')()
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('')
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)
diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts
index dfb51e319..7fde8e62b 100644
--- a/packages/runtime-vapor/src/componentSlots.ts
+++ b/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 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 any>(fn?: T) {
+ if (fn)
+ return (binds?: NormalizedRawProps): ReturnType =>
+ fn(binds && normalizeSlotProps(binds))
+ }
+}
+
+function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
+ const { length } = rawPropsList
+ const mergings = length > 1 ? shallowReactive([]) : undefined
+ const result = shallowReactive({})
+
+ 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({}))
+ : 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]
+ }
+ }
}
diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts
index 2c66815f5..183edc4d5 100644
--- a/packages/runtime-vapor/src/dom/prop.ts
+++ b/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]