Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 180 additions & 35 deletions packages/runtime-vapor/__tests__/componentSlots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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('<div></div>')()
const n2 = template('<div></div>')()
const n3 = template('<div></div>')()
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('<div></div>')()
const n2 = template('<div></div>')()
const n3 = template('<div></div>')()
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('<div></div>')()
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(
'<div>content1<!--slot--></div>' +
'<div>content2<!--slot--></div>' +
'<div>content3<!--slot--></div>' +
'<div>content4<!--slot--></div>' +
'<div>content5<!--slot--></div>',
)

// Update items
items.value = [2, 4]
await nextTick()

expect(host.innerHTML).toBe(
'<div><!--slot--></div>' +
'<div>content2<!--slot--></div>' +
'<div><!--slot--></div>' +
'<div>content4<!--slot--></div>' +
'<div><!--slot--></div>',
)
})
})
})
22 changes: 19 additions & 3 deletions packages/runtime-vapor/src/componentSlots.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -51,9 +52,24 @@ export type StaticSlots = Record<string, VaporSlot>

export type VaporSlot = BlockFn
export type DynamicSlot = { name: string; fn: VaporSlot }
export type DynamicSlotFn = () => DynamicSlot | DynamicSlot[]
export type DynamicSlotFn = (() => DynamicSlot | DynamicSlot[]) & {
_cache?: ComputedRef<DynamicSlot | DynamicSlot[]>
}
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<RawSlots> = {
get: getSlot,
has: (target, key: string) => !!getSlot(target, key),
Expand All @@ -74,7 +90,7 @@ export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
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 {
Expand Down Expand Up @@ -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) {
Expand Down
Loading