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]