Skip to content

Commit fd2d9ff

Browse files
committed
feat(runtime-vapor): dynamic slots
1 parent e967ed1 commit fd2d9ff

File tree

5 files changed

+182
-118
lines changed

5 files changed

+182
-118
lines changed

packages/runtime-vapor/__tests__/componentSlots.spec.ts

Lines changed: 114 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import {
44
createComponent,
5+
createVaporApp,
56
defineComponent,
67
getCurrentInstance,
78
nextTick,
89
ref,
910
template,
1011
} from '../src'
11-
import { createSlots } from '../src/slot'
12+
import { createSlots } from '../src/apiCreateSlots'
1213
import { makeRender } from './_utils'
1314

1415
const define = makeRender<any>()
@@ -69,80 +70,65 @@ describe('component: slots', () => {
6970
render(
7071
{},
7172
createSlots({
72-
header: () => {
73-
const t0 = template('header')
74-
// TODO: single node
75-
return [t0()]
76-
},
73+
header: () => template('header')(),
7774
}),
7875
)
79-
expect(instance.slots.header()).toMatchObject([
76+
expect(instance.slots.header()).toMatchObject(
8077
document.createTextNode('header'),
81-
])
78+
)
8279
})
8380

81+
// TODO: test case name
8482
test('initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', () => {
8583
const { slots } = renderWithSlots(
8684
createSlots({
87-
// TODO: normalize from array
88-
default: () => {
89-
const t0 = template('<span></span>')
90-
return [t0()]
91-
},
85+
// TODO: normalize from array?
86+
default: () => template('<span></span>')(),
9287
}),
9388
)
9489

95-
// TODO: warn
9690
// expect(
9791
// '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
9892
// ).toHaveBeenWarned()
9993

100-
expect(slots.default()).toMatchObject([document.createElement('span')])
94+
expect(slots.default()).toMatchObject(document.createElement('span'))
10195
})
10296

103-
// TODO: dynamic slot
104-
test.todo(
105-
'updateSlots: instance.slots should be updated correctly',
106-
async () => {
107-
const flag1 = ref(true)
97+
test('updateSlots: instance.slots should be updated correctly', async () => {
98+
const flag1 = ref(true)
10899

109-
let instance: any
110-
const Child = () => {
111-
instance = getCurrentInstance()
112-
return template('child')()
113-
}
100+
let instance: any
101+
const Child = () => {
102+
instance = getCurrentInstance()
103+
return template('child')()
104+
}
114105

115-
const { render } = define({
116-
render() {
117-
return createComponent(
118-
Child,
119-
{},
120-
createSlots({
121-
default: () => {
122-
// TODO: dynamic slot
123-
return flag1.value
124-
? [template('<span></span>')()]
125-
: [template('<div></div>')()]
126-
},
127-
}),
128-
)
129-
},
130-
})
106+
const { render } = define({
107+
render() {
108+
return createComponent(
109+
Child,
110+
{},
111+
createSlots({ _: 2 as any }, () => [
112+
flag1.value
113+
? { name: 'one', fn: () => template('<span></span>')() }
114+
: { name: 'two', fn: () => template('<div></div>')() },
115+
]),
116+
)
117+
},
118+
})
131119

132-
render()
120+
render()
133121

134-
expect(instance.slots.default()).toMatchObject([])
135-
expect(instance.slots.default()).not.toMatchObject([])
122+
expect(instance.slots).toHaveProperty('one')
123+
expect(instance.slots).not.toHaveProperty('two')
136124

137-
flag1.value = false
138-
await nextTick()
125+
flag1.value = false
126+
await nextTick()
139127

140-
expect(instance.slots.default()).not.toMatchObject([])
141-
expect(instance.slots.default()).toMatchObject([])
142-
},
143-
)
128+
expect(instance.slots).not.toHaveProperty('one')
129+
expect(instance.slots).toHaveProperty('two')
130+
})
144131

145-
// TODO: dynamic slots
146132
test.todo(
147133
'updateSlots: instance.slots should be updated correctly',
148134
async () => {
@@ -164,16 +150,15 @@ describe('component: slots', () => {
164150
}
165151

166152
const { render } = define({
167-
render() {
168-
const t0 = template('<div></div>')
169-
const n0 = t0()
170-
// renderComponent(
171-
// Child,
172-
// {},
173-
// createSlots(flag1.value ? oldSlots : newSlots),
174-
// n0 as ParentNode,
175-
// )
176-
return []
153+
setup() {
154+
return (() => {
155+
return createComponent(
156+
Child,
157+
{},
158+
// TODO: maybe it is not supported
159+
createSlots(flag1.value ? oldSlots : newSlots),
160+
)
161+
})()
177162
},
178163
})
179164

@@ -188,56 +173,78 @@ describe('component: slots', () => {
188173
},
189174
)
190175

191-
test.todo(
192-
'updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)',
193-
async () => {
194-
// TODO: dynamic slots
195-
},
196-
)
176+
// TODO: test case name
177+
test('updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)', async () => {
178+
const flag1 = ref(true)
179+
180+
let instance: any
181+
const Child = () => {
182+
instance = getCurrentInstance()
183+
return template('child')()
184+
}
185+
186+
const { render } = define({
187+
setup() {
188+
return createComponent(
189+
Child,
190+
{},
191+
createSlots({}, () => [
192+
flag1.value
193+
? [{ name: 'header', fn: () => template('header')() }]
194+
: [{ name: 'footer', fn: () => template('footer')() }],
195+
]),
196+
)
197+
},
198+
})
199+
render()
200+
201+
expect(instance.slots).toHaveProperty('header')
202+
flag1.value = false
203+
await nextTick()
204+
205+
// expect(
206+
// '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
207+
// ).toHaveBeenWarned()
208+
209+
expect(instance.slots).toHaveProperty('footer')
210+
})
197211

198212
test.todo('should respect $stable flag', async () => {
199-
// TODO: $stable flag
213+
// TODO: $stable flag?
200214
})
201215

202216
test.todo('should not warn when mounting another app in setup', () => {
203-
// TODO: warning and createApp fn
204-
// const Comp = {
205-
// render() {
206-
// const i = getCurrentInstance()
207-
// return i?.slots.default?.()
208-
// },
209-
// }
210-
// const mountComp = () => {
211-
// createApp({
212-
// render() {
213-
// const t0 = template('<div></div>')
214-
// const n0 = t0()
215-
// renderComponent(
216-
// Comp,
217-
// {},
218-
// createSlots({
219-
// default: () => {
220-
// const t0 = template('msg')
221-
// return [t0()]
222-
// },
223-
// }),
224-
// n0,
225-
// )
226-
// return n0
227-
// },
228-
// })
229-
// }
230-
// const App = {
231-
// setup() {
232-
// mountComp()
233-
// },
234-
// render() {
235-
// return null
236-
// },
237-
// }
238-
// createApp(App).mount(document.createElement('div'))
239-
// expect(
240-
// 'Slot "default" invoked outside of the render function',
241-
// ).not.toHaveBeenWarned()
217+
// TODO: warning
218+
const Comp = defineComponent({
219+
render() {
220+
const i = getCurrentInstance()
221+
return i!.slots.default!()
222+
},
223+
})
224+
const mountComp = () => {
225+
createVaporApp({
226+
render() {
227+
return createComponent(
228+
Comp,
229+
{},
230+
createSlots({
231+
default: () => template('msg')(),
232+
}),
233+
)!
234+
},
235+
})
236+
}
237+
const App = {
238+
setup() {
239+
mountComp()
240+
},
241+
render() {
242+
return null!
243+
},
244+
}
245+
createVaporApp(App).mount(document.createElement('div'))
246+
expect(
247+
'Slot "default" invoked outside of the render function',
248+
).not.toHaveBeenWarned()
242249
})
243250
})
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// NOTE: this filed is based on `runtime-core/src/helpers/createSlots.ts`
2+
3+
import { EMPTY_ARR, isArray } from '@vue/shared'
4+
import { renderWatch } from './renderWatch'
5+
import type { InternalSlots, Slot } from './componentSlots'
6+
7+
// TODO: SSR
8+
9+
interface CompiledSlotDescriptor {
10+
name: string
11+
fn: Slot
12+
key?: string
13+
}
14+
15+
export const createSlots = (
16+
slots: InternalSlots,
17+
dynamicSlotsGetter?: () => (
18+
| CompiledSlotDescriptor
19+
| CompiledSlotDescriptor[]
20+
| undefined
21+
)[],
22+
): InternalSlots => {
23+
const dynamicSlotKeys: Record<string, true> = {}
24+
renderWatch(
25+
() => dynamicSlotsGetter?.() ?? EMPTY_ARR,
26+
dynamicSlots => {
27+
for (let i = 0; i < dynamicSlots.length; i++) {
28+
const slot = dynamicSlots[i]
29+
// array of dynamic slot generated by <template v-for="..." #[...]>
30+
if (isArray(slot)) {
31+
for (let j = 0; j < slot.length; j++) {
32+
slots[slot[j].name] = slot[j].fn
33+
dynamicSlotKeys[slot[j].name] = true
34+
}
35+
} else if (slot) {
36+
// conditional single slot generated by <template v-if="..." #foo>
37+
slots[slot.name] = slot.key
38+
? (...args: any[]) => {
39+
const res = slot.fn(...args)
40+
// attach branch key so each conditional branch is considered a
41+
// different fragment
42+
if (res) (res as any).key = slot.key
43+
return res
44+
}
45+
: slot.fn
46+
dynamicSlotKeys[slot.name] = true
47+
}
48+
}
49+
50+
// delete stale slots
51+
for (const key in dynamicSlotKeys) {
52+
if (
53+
// TODO: type (renderWatch)
54+
!dynamicSlots.some((slot: any) =>
55+
isArray(slot) ? slot.some(s => s.name === key) : slot?.name === key,
56+
)
57+
) {
58+
delete slots[key]
59+
}
60+
}
61+
},
62+
{ immediate: true },
63+
)
64+
return slots
65+
}

packages/runtime-vapor/src/componentSlots.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { Block } from './apiRender'
44

55
export type Slot<T extends any = any> = (
66
...args: IfAny<T, any[], [T] | (T extends undefined ? [] : never)>
7-
) => Block[]
7+
) => Block
88

99
export type InternalSlots = {
1010
[name: string]: Slot | undefined
@@ -14,11 +14,9 @@ export type Slots = Readonly<InternalSlots>
1414

1515
export const initSlots = (
1616
instance: ComponentInternalInstance,
17-
slots: Slots | null,
17+
slots: InternalSlots | null,
1818
) => {
1919
if (!slots) slots = {}
2020
// TODO: normalize?
2121
instance.slots = slots
2222
}
23-
24-
// TODO: $stable ?

packages/runtime-vapor/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export {
110110
export { createIf } from './apiCreateIf'
111111
export { createFor } from './apiCreateFor'
112112
export { createComponent } from './apiCreateComponent'
113-
export { createSlots } from './slot'
113+
export { createSlots } from './apiCreateSlots'
114114
export { resolveComponent, resolveDirective } from './helpers/resolveAssets'
115115

116116
// **Internal** DOM-only runtime directive helpers

packages/runtime-vapor/src/slot.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)