diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index db2171b583a..33b7f7f2f2f 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -103,41 +103,6 @@ describe('component: slots', () => { expect(instance.slots).toHaveProperty('two') }) - test('should work with createFlorSlots', async () => { - const loop = ref([1, 2, 3]) - - let instance: any - const Child = () => { - instance = currentInstance - return template('child')() - } - - const { render } = define({ - setup() { - return createComponent(Child, null, { - $: [ - () => - createForSlots(loop.value, (item, i) => ({ - name: item, - fn: () => template(item + i)(), - })), - ], - }) - }, - }) - render() - - expect(instance.slots).toHaveProperty('1') - expect(instance.slots).toHaveProperty('2') - expect(instance.slots).toHaveProperty('3') - loop.value.push(4) - await nextTick() - expect(instance.slots).toHaveProperty('4') - loop.value.shift() - await nextTick() - expect(instance.slots).not.toHaveProperty('1') - }) - // passes but no warning for slot invocation in vapor currently test.todo('should not warn when mounting another app in setup', () => { const Comp = defineVaporComponent({ @@ -1936,4 +1901,184 @@ describe('component: slots', () => { }) }) }) + + describe('createForSlots', () => { + test('should work', async () => { + const loop = ref([1, 2, 3]) + + let instance: any + const Child = () => { + instance = currentInstance + return template('child')() + } + + const { render } = define({ + setup() { + return createComponent(Child, null, { + $: [ + () => + createForSlots(loop.value, (item, i) => ({ + name: item, + fn: () => template(item + i)(), + })), + ], + }) + }, + }) + render() + + expect(instance.slots).toHaveProperty('1') + expect(instance.slots).toHaveProperty('2') + expect(instance.slots).toHaveProperty('3') + loop.value.push(4) + await nextTick() + expect(instance.slots).toHaveProperty('4') + loop.value.shift() + await nextTick() + expect(instance.slots).not.toHaveProperty('1') + }) + + test('should cache dynamic slot source result', async () => { + const items = ref([1, 2, 3]) + let callCount = 0 + + const getItems = () => { + callCount++ + return items.value + } + + let instance: any + const Child = defineVaporComponent(() => { + instance = currentInstance + // Create multiple slots to trigger multiple getSlot calls + const n1 = template('
')() + const n2 = template('
')() + const n3 = template('
')() + insert(createSlot('slot1'), n1 as any as ParentNode) + insert(createSlot('slot2'), n2 as any as ParentNode) + insert(createSlot('slot3'), n3 as any as ParentNode) + return [n1, n2, n3] + }) + + define({ + setup() { + return createComponent(Child, null, { + $: [ + () => + createForSlots(getItems(), (item, i) => ({ + name: 'slot' + item, + fn: () => template(String(item))(), + })), + ], + }) + }, + }).render() + + // getItems should only be called once + expect(callCount).toBe(1) + + expect(instance.slots).toHaveProperty('slot1') + expect(instance.slots).toHaveProperty('slot2') + expect(instance.slots).toHaveProperty('slot3') + }) + + test('should update when source changes', async () => { + const items = ref([1, 2]) + let callCount = 0 + + const getItems = () => { + callCount++ + return items.value + } + + let instance: any + const Child = defineVaporComponent(() => { + instance = currentInstance + const n1 = template('
')() + const n2 = template('
')() + const n3 = template('
')() + insert(createSlot('slot1'), n1 as any as ParentNode) + insert(createSlot('slot2'), n2 as any as ParentNode) + insert(createSlot('slot3'), n3 as any as ParentNode) + return [n1, n2, n3] + }) + + define({ + setup() { + return createComponent(Child, null, { + $: [ + () => + createForSlots(getItems(), (item, i) => ({ + name: 'slot' + item, + fn: () => template(String(item))(), + })), + ], + }) + }, + }).render() + + expect(callCount).toBe(1) + expect(instance.slots).toHaveProperty('slot1') + expect(instance.slots).toHaveProperty('slot2') + expect(instance.slots).not.toHaveProperty('slot3') + + // Update items + items.value.push(3) + await nextTick() + + // Should be called again after source changes + expect(callCount).toBe(2) + expect(instance.slots).toHaveProperty('slot1') + expect(instance.slots).toHaveProperty('slot2') + expect(instance.slots).toHaveProperty('slot3') + }) + + test('should render slots correctly with caching', async () => { + const items = ref([1, 2, 3, 4, 5]) + + const Child = defineVaporComponent(() => { + const containers: any[] = [] + for (let i = 1; i <= 5; i++) { + const n = template('
')() + insert(createSlot('slot' + i), n as any as ParentNode) + containers.push(n) + } + return containers + }) + + const { host } = define({ + setup() { + return createComponent(Child, null, { + $: [ + () => + createForSlots(items.value, item => ({ + name: 'slot' + item, + fn: () => template('content' + item)(), + })), + ], + }) + }, + }).render() + + expect(host.innerHTML).toBe( + '
content1
' + + '
content2
' + + '
content3
' + + '
content4
' + + '
content5
', + ) + + // Update items + items.value = [2, 4] + await nextTick() + + expect(host.innerHTML).toBe( + '
' + + '
content2
' + + '
' + + '
content4
' + + '
', + ) + }) + }) }) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index b0c50917f64..dfd22c23b28 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -1,4 +1,5 @@ import { EMPTY_OBJ, NO, hasOwn, isArray, isFunction } from '@vue/shared' +import { type ComputedRef, computed } from '@vue/reactivity' import { type Block, type BlockFn, insert, setScopeId } from './block' import { rawPropsProxyHandlers } from './componentProps' import { @@ -51,9 +52,24 @@ export type StaticSlots = Record export type VaporSlot = BlockFn export type DynamicSlot = { name: string; fn: VaporSlot } -export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[] +export type DynamicSlotFn = (() => DynamicSlot | DynamicSlot[]) & { + _cache?: ComputedRef +} export type DynamicSlotSource = StaticSlots | DynamicSlotFn +/** + * Get cached result of a DynamicSlotFn. + * Uses computed to cache the result and avoid redundant calls. + */ +function resolveDynamicSlot( + source: DynamicSlotFn, +): DynamicSlot | DynamicSlot[] { + if (!source._cache) { + source._cache = computed(source) + } + return source._cache.value +} + export const dynamicSlotsProxyHandlers: ProxyHandler = { get: getSlot, has: (target, key: string) => !!getSlot(target, key), @@ -74,7 +90,7 @@ export const dynamicSlotsProxyHandlers: ProxyHandler = { keys = keys.filter(k => k !== '$') for (const source of dynamicSources) { if (isFunction(source)) { - const slot = source() + const slot = resolveDynamicSlot(source) if (isArray(slot)) { for (const s of slot) keys.push(String(s.name)) } else { @@ -103,7 +119,7 @@ export function getSlot( while (i--) { source = dynamicSources[i] if (isFunction(source)) { - const slot = source() + const slot = resolveDynamicSlot(source) if (slot) { if (isArray(slot)) { for (const s of slot) {