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 {