diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 987130820..62e99da38 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -1,7 +1,9 @@ // NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`. import { + type ComponentInternalInstance, createComponent, + createFor, createForSlots, createSlot, createVaporApp, @@ -14,6 +16,7 @@ import { renderEffect, setText, template, + useSlots, withDestructure, } from '../src' import { makeRender } from './_utils' @@ -325,6 +328,181 @@ describe('component: slots', () => { expect(instance.slots).not.toHaveProperty('1') }) + test('should not delete new rendered slot when the old slot is removed in loop slot', async () => { + const loop = ref([1, 'default', 3]) + + let childInstance + const t0 = template('
') + const { component: Child } = define({ + setup() { + childInstance = getCurrentInstance() + const slots = useSlots() + const keys = () => Object.keys(slots) + return { + keys, + slots, + } + }, + render: (_ctx: any) => { + const n0 = createFor( + () => _ctx.keys(), + (_ctx0: any) => { + const n5 = t0() + const n4 = createSlot(() => _ctx0[0]) + insert(n4, n5 as ParentNode) + return n5 + }, + ) + return n0 + }, + }) + + const t1 = template(' static default ') + const { render } = define({ + setup() { + return createComponent(Child, {}, [ + { + default: () => { + return t1() + }, + }, + () => + createForSlots(loop.value, (item, i) => ({ + name: item, + fn: () => template(item)(), + })), + ]) + }, + }) + const { html } = render() + + expect(childInstance!.slots).toHaveProperty('1') + expect(childInstance!.slots).toHaveProperty('default') + expect(childInstance!.slots).toHaveProperty('3') + expect(html()).toBe( + '
1
3
default
', + ) + loop.value = [1] + await nextTick() + expect(childInstance!.slots).toHaveProperty('1') + expect(childInstance!.slots).toHaveProperty('default') + expect(childInstance!.slots).not.toHaveProperty('3') + expect(html()).toBe( + '
1
static default
', + ) + }) + + test('should cleanup all slots when loop slot has same key', async () => { + const loop = ref([1, 1, 1]) + + let childInstance + const t0 = template('
') + const { component: Child } = define({ + setup() { + childInstance = getCurrentInstance() + const slots = useSlots() + const keys = () => Object.keys(slots) + return { + keys, + slots, + } + }, + render: (_ctx: any) => { + const n0 = createFor( + () => _ctx.keys(), + (_ctx0: any) => { + const n5 = t0() + const n4 = createSlot(() => _ctx0[0]) + insert(n4, n5 as ParentNode) + return n5 + }, + ) + return n0 + }, + }) + + const t1 = template(' static default ') + const { render } = define({ + setup() { + return createComponent(Child, {}, [ + { + default: () => { + return t1() + }, + }, + () => + createForSlots(loop.value, (item, i) => ({ + name: item, + fn: () => template(item)(), + })), + ]) + }, + }) + const { html } = render() + expect(childInstance!.slots).toHaveProperty('1') + expect(childInstance!.slots).toHaveProperty('default') + expect(html()).toBe( + '
1
static default
', + ) + loop.value = [1] + await nextTick() + expect(childInstance!.slots).toHaveProperty('1') + expect(childInstance!.slots).toHaveProperty('default') + expect(html()).toBe( + '
1
static default
', + ) + loop.value = [1, 2, 3] + await nextTick() + expect(childInstance!.slots).toHaveProperty('1') + expect(childInstance!.slots).toHaveProperty('2') + expect(childInstance!.slots).toHaveProperty('3') + expect(childInstance!.slots).toHaveProperty('default') + expect(html()).toBe( + '
1
2
3
static default
', + ) + }) + + test('dynamicSlots should not cover high level slots', async () => { + const dynamicFlag = ref(true) + + let instance: ComponentInternalInstance + const { component: Child } = define({ + render() { + instance = getCurrentInstance()! + return [createSlot('default'), createSlot('others')] + }, + }) + + const { render, html } = define({ + render() { + return createComponent(Child, {}, [ + () => + dynamicFlag.value + ? { name: 'default', fn: () => template('dynamic default')() } + : { name: 'others', fn: () => template('others')() }, + { + default: () => template('default')(), + }, + ]) + }, + }) + + render() + + expect(html()).toBe('default') + + dynamicFlag.value = false + await nextTick() + + expect(html()).toBe('defaultothers') + expect(instance!.slots).haveOwnProperty('others') + + dynamicFlag.value = true + await nextTick() + expect(html()).toBe('default') + expect(instance!.slots).not.haveOwnProperty('others') + }) + test.todo('should respect $stable flag', async () => { // TODO: $stable flag? }) diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 87d57ec8e..7e09490ec 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -45,51 +45,86 @@ export function initSlots( // with ctx const slots = rawSlots[0] as StaticSlots for (const name in slots) { - registerSlot(name, slots[name]) + addSlot(name, slots[name]) } return } instance.slots = shallowReactive({}) - const keys: Set[] = [] + /** + * Maintain a queue for each slot name, so that we can + * render the next slot when the highest level slot was removed + * + * |- key1: [level2, slot2], [level1, slot1] + * | + * |- key2: [level3, slot3] + * | + * |... + * + * For example, if level2 slot re-rendered and it turns out to render key3 + * then we need to take out level1 slot and render in key1 + */ + const slotsQueue: Record = {} rawSlots.forEach((slots, index) => { const isDynamicSlot = isDynamicSlotFn(slots) if (isDynamicSlot) { firstEffect(instance, () => { - const recordNames = keys[index] || (keys[index] = new Set()) - let dynamicSlot: ReturnType - if (isDynamicSlotFn(slots)) { - dynamicSlot = slots() - if (isArray(dynamicSlot)) { - for (const slot of dynamicSlot) { - registerSlot(slot.name, slot.fn, recordNames) - } - } else if (dynamicSlot) { - registerSlot(dynamicSlot.name, dynamicSlot.fn, recordNames) + let dynamicSlot = slots() + // cleanup slots and re-calc to avoid diffing slots between renders + // cleanup will return a slotNames array contains the slot names that need to be restored + const restoreSlotNames = cleanupSlot(index) + if (isArray(dynamicSlot)) { + for (const slot of dynamicSlot) { + registerSlot(slot.name, slot.fn, index) } - } else { + } else if (dynamicSlot) { + registerSlot(dynamicSlot.name, dynamicSlot.fn, index) } - for (const name of recordNames) { - if ( - !(isArray(dynamicSlot) - ? dynamicSlot.some(s => s.name === name) - : dynamicSlot && dynamicSlot.name === name) - ) { - recordNames.delete(name) - delete instance.slots[name] + // restore after re-calc slots + if (restoreSlotNames.length) { + for (const key of restoreSlotNames) { + const [_, restoreFn] = slotsQueue[key][0] + addSlot(key, restoreFn) } } }) } else { for (const name in slots) { - registerSlot(name, slots[name]) + registerSlot(name, slots[name], index) } } }) - function registerSlot(name: string, fn: Slot, recordNames?: Set) { + function cleanupSlot(level: number) { + const restoreSlotNames: string[] = [] + // remove slots from all queues + Object.keys(slotsQueue).forEach(slotName => { + const index = slotsQueue[slotName].findIndex(([l]) => l === level) + if (index > -1) { + slotsQueue[slotName] = slotsQueue[slotName].filter(([l]) => l !== level) + if (!slotsQueue[slotName].length) { + delete slotsQueue[slotName] + delete instance.slots[slotName] + return + } + // restore next slot if the removed slots was the highest level slot + if (index === 0) { + restoreSlotNames.push(slotName) + } + } + }) + return restoreSlotNames + } + + function registerSlot(name: string, slot: Slot, level: number) { + slotsQueue[name] ||= [] + slotsQueue[name].push([level, slot]) + slotsQueue[name].sort((a, b) => b[0] - a[0]) + addSlot(name, slotsQueue[name][0][1]) + } + + function addSlot(name: string, fn: Slot) { instance.slots[name] = withCtx(fn) - recordNames && recordNames.add(name) } function withCtx(fn: Slot): Slot {