Skip to content

Commit 78f74ce

Browse files
ubugeeeisxzz
andauthored
feat(runtime-vapor): component slot (#143)
Co-authored-by: 三咲智子 Kevin Deng <[email protected]>
1 parent bd888b9 commit 78f74ce

File tree

10 files changed

+411
-6
lines changed

10 files changed

+411
-6
lines changed

Diff for: packages/compiler-vapor/src/generators/component.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { genExpression } from './expression'
1313
import { genPropKey } from './prop'
1414

15+
// TODO: generate component slots
1516
export function genCreateComponent(
1617
oper: CreateComponentIRNode,
1718
context: CodegenContext,

Diff for: packages/runtime-vapor/__tests__/apiInject.spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
createComponent,
77
createTextNode,
88
createVaporApp,
9-
getCurrentInstance,
109
hasInjectionContext,
1110
inject,
1211
nextTick,

Diff for: packages/runtime-vapor/__tests__/componentAttrs.spec.ts

+8
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ describe('attribute fallthrough', () => {
4040
id: () => _ctx.id,
4141
},
4242
],
43+
null,
44+
null,
4345
true,
4446
)
4547
},
@@ -85,6 +87,8 @@ describe('attribute fallthrough', () => {
8587
id: () => _ctx.id,
8688
},
8789
],
90+
null,
91+
null,
8892
true,
8993
)
9094
},
@@ -123,6 +127,8 @@ describe('attribute fallthrough', () => {
123127
'custom-attr': () => 'custom-attr',
124128
},
125129
],
130+
null,
131+
null,
126132
true,
127133
)
128134
return n0
@@ -144,6 +150,8 @@ describe('attribute fallthrough', () => {
144150
id: () => _ctx.id,
145151
},
146152
],
153+
null,
154+
null,
147155
true,
148156
)
149157
},

Diff for: packages/runtime-vapor/__tests__/componentProps.spec.ts

+2
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ describe('component: props', () => {
244244
foo: () => _ctx.foo,
245245
id: () => _ctx.id,
246246
},
247+
null,
248+
null,
247249
true,
248250
)
249251
},
+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// NOTE: This test is implemented based on the case of `runtime-core/__test__/componentSlots.spec.ts`.
2+
3+
import {
4+
createComponent,
5+
createVaporApp,
6+
defineComponent,
7+
getCurrentInstance,
8+
nextTick,
9+
ref,
10+
template,
11+
} from '../src'
12+
import { makeRender } from './_utils'
13+
14+
const define = makeRender<any>()
15+
function renderWithSlots(slots: any): any {
16+
let instance: any
17+
const Comp = defineComponent({
18+
render() {
19+
const t0 = template('<div></div>')
20+
const n0 = t0()
21+
instance = getCurrentInstance()
22+
return n0
23+
},
24+
})
25+
26+
const { render } = define({
27+
render() {
28+
return createComponent(Comp, {}, slots)
29+
},
30+
})
31+
32+
render()
33+
return instance
34+
}
35+
36+
describe('component: slots', () => {
37+
test('initSlots: instance.slots should be set correctly', () => {
38+
const { slots } = renderWithSlots({ _: 1 })
39+
expect(slots).toMatchObject({ _: 1 })
40+
})
41+
42+
// NOTE: slot normalization is not supported
43+
test.todo(
44+
'initSlots: should normalize object slots (when value is null, string, array)',
45+
() => {},
46+
)
47+
test.todo(
48+
'initSlots: should normalize object slots (when value is function)',
49+
() => {},
50+
)
51+
52+
test('initSlots: instance.slots should be set correctly', () => {
53+
let instance: any
54+
const Comp = defineComponent({
55+
render() {
56+
const t0 = template('<div></div>')
57+
const n0 = t0()
58+
instance = getCurrentInstance()
59+
return n0
60+
},
61+
})
62+
63+
const { render } = define({
64+
render() {
65+
return createComponent(Comp, {}, { header: () => template('header')() })
66+
},
67+
})
68+
69+
render()
70+
71+
expect(instance.slots.header()).toMatchObject(
72+
document.createTextNode('header'),
73+
)
74+
})
75+
76+
// runtime-core's "initSlots: instance.slots should be set correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
77+
test('initSlots: instance.slots should be set correctly', () => {
78+
const { slots } = renderWithSlots({
79+
default: () => template('<span></span>')(),
80+
})
81+
82+
// expect(
83+
// '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
84+
// ).toHaveBeenWarned()
85+
86+
expect(slots.default()).toMatchObject(document.createElement('span'))
87+
})
88+
89+
test('updateSlots: instance.slots should be updated correctly', async () => {
90+
const flag1 = ref(true)
91+
92+
let instance: any
93+
const Child = () => {
94+
instance = getCurrentInstance()
95+
return template('child')()
96+
}
97+
98+
const { render } = define({
99+
render() {
100+
return createComponent(Child, {}, { _: 2 as any }, () => [
101+
flag1.value
102+
? { name: 'one', fn: () => template('<span></span>')() }
103+
: { name: 'two', fn: () => template('<div></div>')() },
104+
])
105+
},
106+
})
107+
108+
render()
109+
110+
expect(instance.slots).toHaveProperty('one')
111+
expect(instance.slots).not.toHaveProperty('two')
112+
113+
flag1.value = false
114+
await nextTick()
115+
116+
expect(instance.slots).not.toHaveProperty('one')
117+
expect(instance.slots).toHaveProperty('two')
118+
})
119+
120+
// NOTE: it is not supported
121+
// test('updateSlots: instance.slots should be updated correctly (when slotType is null)', () => {})
122+
123+
// runtime-core's "updateSlots: instance.slots should be update correctly (when vnode.shapeFlag is not SLOTS_CHILDREN)"
124+
test('updateSlots: instance.slots should be update correctly', async () => {
125+
const flag1 = ref(true)
126+
127+
let instance: any
128+
const Child = () => {
129+
instance = getCurrentInstance()
130+
return template('child')()
131+
}
132+
133+
const { render } = define({
134+
setup() {
135+
return createComponent(Child, {}, {}, () => [
136+
flag1.value
137+
? [{ name: 'header', fn: () => template('header')() }]
138+
: [{ name: 'footer', fn: () => template('footer')() }],
139+
])
140+
},
141+
})
142+
render()
143+
144+
expect(instance.slots).toHaveProperty('header')
145+
flag1.value = false
146+
await nextTick()
147+
148+
// expect(
149+
// '[Vue warn]: Non-function value encountered for default slot. Prefer function slots for better performance.',
150+
// ).toHaveBeenWarned()
151+
152+
expect(instance.slots).toHaveProperty('footer')
153+
})
154+
155+
test.todo('should respect $stable flag', async () => {
156+
// TODO: $stable flag?
157+
})
158+
159+
test.todo('should not warn when mounting another app in setup', () => {
160+
// TODO: warning
161+
const Comp = defineComponent({
162+
render() {
163+
const i = getCurrentInstance()
164+
return i!.slots.default!()
165+
},
166+
})
167+
const mountComp = () => {
168+
createVaporApp({
169+
render() {
170+
return createComponent(
171+
Comp,
172+
{},
173+
{ default: () => template('msg')() },
174+
)!
175+
},
176+
})
177+
}
178+
const App = {
179+
setup() {
180+
mountComp()
181+
},
182+
render() {
183+
return null!
184+
},
185+
}
186+
createVaporApp(App).mount(document.createElement('div'))
187+
expect(
188+
'Slot "default" invoked outside of the render function',
189+
).not.toHaveBeenWarned()
190+
})
191+
})

Diff for: packages/runtime-vapor/src/apiCreateComponent.ts

+5
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,22 @@ import {
55
} from './component'
66
import { setupComponent } from './apiRender'
77
import type { RawProps } from './componentProps'
8+
import type { DynamicSlots, Slots } from './componentSlots'
89
import { withAttrs } from './componentAttrs'
910

1011
export function createComponent(
1112
comp: Component,
1213
rawProps: RawProps | null = null,
14+
slots: Slots | null = null,
15+
dynamicSlots: DynamicSlots | null = null,
1316
singleRoot: boolean = false,
1417
) {
1518
const current = currentInstance!
1619
const instance = createComponentInstance(
1720
comp,
1821
singleRoot ? withAttrs(rawProps) : rawProps,
22+
slots,
23+
dynamicSlots,
1924
)
2025
setupComponent(instance, singleRoot)
2126

Diff for: packages/runtime-vapor/src/apiCreateVaporApp.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ export function createVaporApp(
4141

4242
mount(rootContainer): any {
4343
if (!instance) {
44-
instance = createComponentInstance(rootComponent, rootProps, context)
44+
instance = createComponentInstance(
45+
rootComponent,
46+
rootProps,
47+
null,
48+
null,
49+
context,
50+
)
4551
setupComponent(instance)
4652
render(instance, rootContainer)
4753
return instance

Diff for: packages/runtime-vapor/src/component.ts

+32-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import {
1717
emit,
1818
normalizeEmitsOptions,
1919
} from './componentEmits'
20+
import {
21+
type DynamicSlots,
22+
type InternalSlots,
23+
type Slots,
24+
initSlots,
25+
} from './componentSlots'
2026
import { VaporLifecycleHooks } from './apiLifecycle'
2127
import { warn } from './warning'
2228
import { type AppContext, createAppContext } from './apiCreateVaporApp'
@@ -32,7 +38,7 @@ export type SetupContext<E = EmitsOptions> = E extends any
3238
attrs: Data
3339
emit: EmitFn<E>
3440
expose: (exposed?: Record<string, any>) => void
35-
// TODO slots
41+
slots: Readonly<InternalSlots>
3642
}
3743
: never
3844

@@ -46,6 +52,9 @@ export function createSetupContext(
4652
get attrs() {
4753
return getAttrsProxy(instance)
4854
},
55+
get slots() {
56+
return getSlotsProxy(instance)
57+
},
4958
get emit() {
5059
return (event: string, ...args: any[]) => instance.emit(event, ...args)
5160
},
@@ -57,6 +66,7 @@ export function createSetupContext(
5766
return getAttrsProxy(instance)
5867
},
5968
emit: instance.emit,
69+
slots: instance.slots,
6070
expose: NOOP,
6171
}
6272
}
@@ -102,9 +112,11 @@ export interface ComponentInternalInstance {
102112
emit: EmitFn
103113
emitted: Record<string, boolean> | null
104114
attrs: Data
115+
slots: InternalSlots
105116
refs: Data
106117

107-
attrsProxy: Data | null
118+
attrsProxy?: Data
119+
slotsProxy?: Slots
108120

109121
// lifecycle
110122
isMounted: boolean
@@ -188,6 +200,8 @@ let uid = 0
188200
export function createComponentInstance(
189201
component: ObjectComponent | FunctionalComponent,
190202
rawProps: RawProps | null,
203+
slots: Slots | null = null,
204+
dynamicSlots: DynamicSlots | null = null,
191205
// application root node only
192206
appContext: AppContext | null = null,
193207
): ComponentInternalInstance {
@@ -224,10 +238,9 @@ export function createComponentInstance(
224238
emit: null!,
225239
emitted: null,
226240
attrs: EMPTY_OBJ,
241+
slots: EMPTY_OBJ,
227242
refs: EMPTY_OBJ,
228243

229-
attrsProxy: null,
230-
231244
// lifecycle
232245
isMounted: false,
233246
isUnmounted: false,
@@ -283,6 +296,7 @@ export function createComponentInstance(
283296
// [VaporLifecycleHooks.SERVER_PREFETCH]: null,
284297
}
285298
initProps(instance, rawProps, !isFunction(component))
299+
initSlots(instance, slots, dynamicSlots)
286300
instance.emit = emit.bind(null, instance)
287301

288302
return instance
@@ -315,3 +329,17 @@ function getAttrsProxy(instance: ComponentInternalInstance): Data {
315329
))
316330
)
317331
}
332+
333+
/**
334+
* Dev-only
335+
*/
336+
function getSlotsProxy(instance: ComponentInternalInstance): Slots {
337+
return (
338+
instance.slotsProxy ||
339+
(instance.slotsProxy = new Proxy(instance.slots, {
340+
get(target, key: string) {
341+
return target[key]
342+
},
343+
}))
344+
)
345+
}

0 commit comments

Comments
 (0)