Skip to content

Commit 6364b10

Browse files
committed
feat(runtime-vapor): createSlot
1 parent 9a33d79 commit 6364b10

File tree

3 files changed

+112
-47
lines changed

3 files changed

+112
-47
lines changed

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
import {
44
createComponent,
5+
createSlot,
56
createVaporApp,
67
defineComponent,
78
getCurrentInstance,
9+
insert,
810
nextTick,
911
ref,
1012
template,
@@ -41,11 +43,13 @@ describe('component: slots', () => {
4143
const t0 = template('<div></div>')
4244
const n0 = t0()
4345
instance = getCurrentInstance()
46+
const n1 = createSlot('header')
47+
insert(n1, n0 as any as ParentNode)
4448
return n0
4549
},
4650
})
4751

48-
const { render } = define({
52+
const { render, host } = define({
4953
render() {
5054
return createComponent(Comp, {}, { header: () => template('header')() })
5155
},
@@ -56,6 +60,8 @@ describe('component: slots', () => {
5660
expect(instance.slots.header()).toMatchObject(
5761
document.createTextNode('header'),
5862
)
63+
64+
expect(host.innerHTML).toBe('<div>header</div>')
5965
})
6066

6167
// NOTE: slot normalization is not supported
Lines changed: 104 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
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+
effectScope,
5+
isReactive,
6+
shallowReactive,
7+
} from '@vue/reactivity'
8+
import {
9+
type ComponentInternalInstance,
10+
currentInstance,
11+
setCurrentInstance,
12+
} from './component'
13+
import { type Block, type Fragment, fragmentKey } from './apiRender'
14+
import { renderEffect } from './renderEffect'
15+
import { createComment, createTextNode, insert, remove } from './dom/element'
616

717
// TODO: SSR
818

@@ -29,7 +39,7 @@ export const initSlots = (
2939
rawSlots: InternalSlots | null = null,
3040
dynamicSlots: DynamicSlots | null = null,
3141
) => {
32-
const slots: InternalSlots = {}
42+
let slots: InternalSlots = {}
3343

3444
for (const key in rawSlots) {
3545
const slot = rawSlots[key]
@@ -39,50 +49,45 @@ export const initSlots = (
3949
}
4050

4151
if (dynamicSlots) {
52+
slots = shallowReactive(slots)
4253
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
54+
renderEffect(() => {
55+
const _dynamicSlots = dynamicSlots()
56+
for (let i = 0; i < _dynamicSlots.length; i++) {
57+
const slot = _dynamicSlots[i]
58+
// array of dynamic slot generated by <template v-for="..." #[...]>
59+
if (isArray(slot)) {
60+
for (let j = 0; j < slot.length; j++) {
61+
slots[slot[j].name] = withCtx(slot[j].fn)
62+
dynamicSlotKeys[slot[j].name] = true
6863
}
64+
} else if (slot) {
65+
// conditional single slot generated by <template v-if="..." #foo>
66+
slots[slot.name] = withCtx(
67+
slot.key
68+
? (...args: any[]) => {
69+
const res = slot.fn(...args)
70+
// attach branch key so each conditional branch is considered a
71+
// different fragment
72+
if (res) (res as any).key = slot.key
73+
return res
74+
}
75+
: slot.fn,
76+
)
77+
dynamicSlotKeys[slot.name] = true
6978
}
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-
}
79+
}
80+
// delete stale slots
81+
for (const key in dynamicSlotKeys) {
82+
if (
83+
!_dynamicSlots.some(slot =>
84+
isArray(slot) ? slot.some(s => s.name === key) : slot?.name === key,
85+
)
86+
) {
87+
delete slots[key]
8188
}
82-
},
83-
undefined,
84-
{ scheduler: createVaporPreScheduler(instance) },
85-
)
89+
}
90+
})
8691
}
8792

8893
instance.slots = slots
@@ -98,3 +103,56 @@ export const initSlots = (
98103
}
99104
}
100105
}
106+
107+
export function createSlot(
108+
name: string | (() => string),
109+
binds?: Record<string, (() => unknown) | undefined>,
110+
fallback?: () => Block,
111+
): Block {
112+
let block: Block | undefined
113+
let branch: Slot | undefined
114+
let oldBranch: Slot | undefined
115+
let parent: ParentNode | undefined | null
116+
let scope: EffectScope | undefined
117+
const isDynamicName = isFunction(name)
118+
const instance = currentInstance!
119+
const { slots } = instance
120+
121+
// When not using dynamic slots, simplify the process to improve performance
122+
if (!isDynamicName && !isReactive(slots)) {
123+
if ((branch = slots[name] || fallback)) {
124+
return branch(binds)
125+
} else {
126+
return []
127+
}
128+
}
129+
130+
const getName = isDynamicName ? name : () => name
131+
const anchor = __DEV__ ? createComment('slot') : createTextNode()
132+
const fragment: Fragment = {
133+
nodes: [],
134+
anchor,
135+
[fragmentKey]: true,
136+
}
137+
138+
// TODO lifecycle hooks
139+
renderEffect(() => {
140+
if ((branch = slots[getName()] || fallback) !== oldBranch) {
141+
parent ||= anchor.parentNode
142+
if (block) {
143+
scope!.stop()
144+
remove(block, parent!)
145+
}
146+
if ((oldBranch = branch)) {
147+
scope = effectScope()
148+
fragment.nodes = block = scope.run(() => branch!(binds))!
149+
parent && insert(block, parent, anchor)
150+
} else {
151+
scope = block = undefined
152+
fragment.nodes = []
153+
}
154+
}
155+
})
156+
157+
return fragment
158+
}

packages/runtime-vapor/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export {
5050
type FunctionalComponent,
5151
type SetupFn,
5252
} from './component'
53+
export { createSlot } from './componentSlots'
5354
export { renderEffect } from './renderEffect'
5455
export {
5556
watch,

0 commit comments

Comments
 (0)