Skip to content

Commit 0792656

Browse files
authored
feat(runtime-vapor): createSlot (#170)
1 parent a0bd0e9 commit 0792656

File tree

3 files changed

+312
-46
lines changed

3 files changed

+312
-46
lines changed

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

+191
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
import {
44
createComponent,
5+
createSlot,
56
createVaporApp,
67
defineComponent,
78
getCurrentInstance,
9+
insert,
810
nextTick,
11+
prepend,
912
ref,
13+
renderEffect,
14+
setText,
1015
template,
1116
} from '../src'
1217
import { makeRender } from './_utils'
@@ -237,4 +242,190 @@ describe('component: slots', () => {
237242
'Slot "default" invoked outside of the render function',
238243
).not.toHaveBeenWarned()
239244
})
245+
246+
describe('createSlot', () => {
247+
test('slot should be render correctly', () => {
248+
const Comp = defineComponent(() => {
249+
const n0 = template('<div></div>')()
250+
insert(createSlot('header'), n0 as any as ParentNode)
251+
return n0
252+
})
253+
254+
const { host } = define(() => {
255+
return createComponent(Comp, {}, { header: () => template('header')() })
256+
}).render()
257+
258+
expect(host.innerHTML).toBe('<div>header</div>')
259+
})
260+
261+
test('slot should be render correctly with binds', async () => {
262+
const Comp = defineComponent(() => {
263+
const n0 = template('<div></div>')()
264+
insert(
265+
createSlot('header', { title: () => 'header' }),
266+
n0 as any as ParentNode,
267+
)
268+
return n0
269+
})
270+
271+
const { host } = define(() => {
272+
return createComponent(
273+
Comp,
274+
{},
275+
{
276+
header: ({ title }) => {
277+
const el = template('<h1></h1>')()
278+
renderEffect(() => {
279+
setText(el, title())
280+
})
281+
return el
282+
},
283+
},
284+
)
285+
}).render()
286+
287+
expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
288+
})
289+
290+
test('dynamic slot should be render correctly with binds', async () => {
291+
const Comp = defineComponent(() => {
292+
const n0 = template('<div></div>')()
293+
prepend(
294+
n0 as any as ParentNode,
295+
createSlot('header', { title: () => 'header' }),
296+
)
297+
return n0
298+
})
299+
300+
const { host } = define(() => {
301+
// dynamic slot
302+
return createComponent(Comp, {}, {}, () => [
303+
{ name: 'header', fn: ({ title }) => template(`${title()}`)() },
304+
])
305+
}).render()
306+
307+
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
308+
})
309+
310+
test('dynamic slot outlet should be render correctly with binds', async () => {
311+
const Comp = defineComponent(() => {
312+
const n0 = template('<div></div>')()
313+
prepend(
314+
n0 as any as ParentNode,
315+
createSlot(
316+
() => 'header', // dynamic slot outlet name
317+
{ title: () => 'header' },
318+
),
319+
)
320+
return n0
321+
})
322+
323+
const { host } = define(() => {
324+
return createComponent(
325+
Comp,
326+
{},
327+
{ header: ({ title }) => template(`${title()}`)() },
328+
)
329+
}).render()
330+
331+
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
332+
})
333+
334+
test('fallback should be render correctly', () => {
335+
const Comp = defineComponent(() => {
336+
const n0 = template('<div></div>')()
337+
insert(
338+
createSlot('header', {}, () => template('fallback')()),
339+
n0 as any as ParentNode,
340+
)
341+
return n0
342+
})
343+
344+
const { host } = define(() => {
345+
return createComponent(Comp, {}, {})
346+
}).render()
347+
348+
expect(host.innerHTML).toBe('<div>fallback</div>')
349+
})
350+
351+
test('dynamic slot should be updated correctly', async () => {
352+
const flag1 = ref(true)
353+
354+
const Child = defineComponent(() => {
355+
const temp0 = template('<p></p>')
356+
const el0 = temp0()
357+
const el1 = temp0()
358+
const slot1 = createSlot('one', {}, () => template('one fallback')())
359+
const slot2 = createSlot('two', {}, () => template('two fallback')())
360+
insert(slot1, el0 as any as ParentNode)
361+
insert(slot2, el1 as any as ParentNode)
362+
return [el0, el1]
363+
})
364+
365+
const { host } = define(() => {
366+
return createComponent(Child, {}, {}, () => [
367+
flag1.value
368+
? { name: 'one', fn: () => template('one content')() }
369+
: { name: 'two', fn: () => template('two content')() },
370+
])
371+
}).render()
372+
373+
expect(host.innerHTML).toBe(
374+
'<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
375+
)
376+
377+
flag1.value = false
378+
await nextTick()
379+
380+
expect(host.innerHTML).toBe(
381+
'<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
382+
)
383+
384+
flag1.value = true
385+
await nextTick()
386+
387+
expect(host.innerHTML).toBe(
388+
'<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
389+
)
390+
})
391+
392+
test('dynamic slot outlet should be updated correctly', async () => {
393+
const slotOutletName = ref('one')
394+
395+
const Child = defineComponent(() => {
396+
const temp0 = template('<p></p>')
397+
const el0 = temp0()
398+
const slot1 = createSlot(
399+
() => slotOutletName.value,
400+
{},
401+
() => template('fallback')(),
402+
)
403+
insert(slot1, el0 as any as ParentNode)
404+
return el0
405+
})
406+
407+
const { host } = define(() => {
408+
return createComponent(
409+
Child,
410+
{},
411+
{
412+
one: () => template('one content')(),
413+
two: () => template('two content')(),
414+
},
415+
)
416+
}).render()
417+
418+
expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
419+
420+
slotOutletName.value = 'two'
421+
await nextTick()
422+
423+
expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
424+
425+
slotOutletName.value = 'none'
426+
await nextTick()
427+
428+
expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
429+
})
430+
})
240431
})
+120-46
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1-
import { type IfAny, isArray } from '@vue/shared'
2-
import { baseWatch } from '@vue/reactivity'
3-
import { type ComponentInternalInstance, setCurrentInstance } from './component'
4-
import type { Block } from './apiRender'
5-
import { createVaporPreScheduler } from './scheduler'
1+
import { type IfAny, isArray, isFunction } from '@vue/shared'
2+
import {
3+
type EffectScope,
4+
ReactiveEffect,
5+
type SchedulerJob,
6+
SchedulerJobFlags,
7+
effectScope,
8+
isReactive,
9+
shallowReactive,
10+
} from '@vue/reactivity'
11+
import {
12+
type ComponentInternalInstance,
13+
currentInstance,
14+
setCurrentInstance,
15+
} from './component'
16+
import { type Block, type Fragment, fragmentKey } from './apiRender'
17+
import { renderEffect } from './renderEffect'
18+
import { createComment, createTextNode, insert, remove } from './dom/element'
19+
import { queueJob } from './scheduler'
20+
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
621

722
// TODO: SSR
823

@@ -29,7 +44,7 @@ export const initSlots = (
2944
rawSlots: InternalSlots | null = null,
3045
dynamicSlots: DynamicSlots | null = null,
3146
) => {
32-
const slots: InternalSlots = {}
47+
let slots: InternalSlots = {}
3348

3449
for (const key in rawSlots) {
3550
const slot = rawSlots[key]
@@ -39,50 +54,56 @@ export const initSlots = (
3954
}
4055

4156
if (dynamicSlots) {
57+
slots = shallowReactive(slots)
4258
const dynamicSlotKeys: Record<string, true> = {}
43-
baseWatch(
44-
() => {
45-
const _dynamicSlots = dynamicSlots()
46-
for (let i = 0; i < _dynamicSlots.length; i++) {
47-
const slot = _dynamicSlots[i]
48-
// array of dynamic slot generated by <template v-for="..." #[...]>
49-
if (isArray(slot)) {
50-
for (let j = 0; j < slot.length; j++) {
51-
slots[slot[j].name] = withCtx(slot[j].fn)
52-
dynamicSlotKeys[slot[j].name] = true
53-
}
54-
} else if (slot) {
55-
// conditional single slot generated by <template v-if="..." #foo>
56-
slots[slot.name] = withCtx(
57-
slot.key
58-
? (...args: any[]) => {
59-
const res = slot.fn(...args)
60-
// attach branch key so each conditional branch is considered a
61-
// different fragment
62-
if (res) (res as any).key = slot.key
63-
return res
64-
}
65-
: slot.fn,
66-
)
67-
dynamicSlotKeys[slot.name] = true
59+
60+
const effect = new ReactiveEffect(() => {
61+
const _dynamicSlots = callWithAsyncErrorHandling(
62+
dynamicSlots,
63+
instance,
64+
VaporErrorCodes.RENDER_FUNCTION,
65+
)
66+
for (let i = 0; i < _dynamicSlots.length; i++) {
67+
const slot = _dynamicSlots[i]
68+
// array of dynamic slot generated by <template v-for="..." #[...]>
69+
if (isArray(slot)) {
70+
for (let j = 0; j < slot.length; j++) {
71+
slots[slot[j].name] = withCtx(slot[j].fn)
72+
dynamicSlotKeys[slot[j].name] = true
6873
}
74+
} else if (slot) {
75+
// conditional single slot generated by <template v-if="..." #foo>
76+
slots[slot.name] = withCtx(
77+
slot.key
78+
? (...args: any[]) => {
79+
const res = slot.fn(...args)
80+
// attach branch key so each conditional branch is considered a
81+
// different fragment
82+
if (res) (res as any).key = slot.key
83+
return res
84+
}
85+
: slot.fn,
86+
)
87+
dynamicSlotKeys[slot.name] = true
6988
}
70-
// delete stale slots
71-
for (const key in dynamicSlotKeys) {
72-
if (
73-
!_dynamicSlots.some(slot =>
74-
isArray(slot)
75-
? slot.some(s => s.name === key)
76-
: slot?.name === key,
77-
)
78-
) {
79-
delete slots[key]
80-
}
89+
}
90+
// delete stale slots
91+
for (const key in dynamicSlotKeys) {
92+
if (
93+
!_dynamicSlots.some(slot =>
94+
isArray(slot) ? slot.some(s => s.name === key) : slot?.name === key,
95+
)
96+
) {
97+
delete slots[key]
8198
}
82-
},
83-
undefined,
84-
{ scheduler: createVaporPreScheduler(instance) },
85-
)
99+
}
100+
})
101+
102+
const job: SchedulerJob = () => effect.run()
103+
job.flags! |= SchedulerJobFlags.PRE
104+
job.id = instance.uid
105+
effect.scheduler = () => queueJob(job)
106+
effect.run()
86107
}
87108

88109
instance.slots = slots
@@ -98,3 +119,56 @@ export const initSlots = (
98119
}
99120
}
100121
}
122+
123+
export function createSlot(
124+
name: string | (() => string),
125+
binds?: Record<string, (() => unknown) | undefined>,
126+
fallback?: () => Block,
127+
): Block {
128+
let block: Block | undefined
129+
let branch: Slot | undefined
130+
let oldBranch: Slot | undefined
131+
let parent: ParentNode | undefined | null
132+
let scope: EffectScope | undefined
133+
const isDynamicName = isFunction(name)
134+
const instance = currentInstance!
135+
const { slots } = instance
136+
137+
// When not using dynamic slots, simplify the process to improve performance
138+
if (!isDynamicName && !isReactive(slots)) {
139+
if ((branch = slots[name] || fallback)) {
140+
return branch(binds)
141+
} else {
142+
return []
143+
}
144+
}
145+
146+
const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
147+
const anchor = __DEV__ ? createComment('slot') : createTextNode()
148+
const fragment: Fragment = {
149+
nodes: [],
150+
anchor,
151+
[fragmentKey]: true,
152+
}
153+
154+
// TODO lifecycle hooks
155+
renderEffect(() => {
156+
if ((branch = getSlot() || fallback) !== oldBranch) {
157+
parent ||= anchor.parentNode
158+
if (block) {
159+
scope!.stop()
160+
remove(block, parent!)
161+
}
162+
if ((oldBranch = branch)) {
163+
scope = effectScope()
164+
fragment.nodes = block = scope.run(() => branch!(binds))!
165+
parent && insert(block, parent, anchor)
166+
} else {
167+
scope = block = undefined
168+
fragment.nodes = []
169+
}
170+
}
171+
})
172+
173+
return fragment
174+
}

0 commit comments

Comments
 (0)