Skip to content

Commit c9241da

Browse files
authored
feat(runtime-vapor): slot props (#227)
1 parent b023b9b commit c9241da

File tree

3 files changed

+206
-20
lines changed

3 files changed

+206
-20
lines changed

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

Lines changed: 141 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
renderEffect,
1414
setText,
1515
template,
16+
withDestructure,
1617
} from '../src'
1718
import { makeRender } from './_utils'
1819

@@ -319,7 +320,7 @@ describe('component: slots', () => {
319320
const Comp = defineComponent(() => {
320321
const n0 = template('<div></div>')()
321322
insert(
322-
createSlot('header', { title: () => 'header' }),
323+
createSlot('header', [{ title: () => 'header' }]),
323324
n0 as any as ParentNode,
324325
)
325326
return n0
@@ -330,26 +331,150 @@ describe('component: slots', () => {
330331
Comp,
331332
{},
332333
{
333-
header: ({ title }) => {
334-
const el = template('<h1></h1>')()
335-
renderEffect(() => {
336-
setText(el, title())
337-
})
338-
return el
339-
},
334+
header: withDestructure(
335+
({ title }) => [title],
336+
ctx => {
337+
const el = template('<h1></h1>')()
338+
renderEffect(() => {
339+
setText(el, ctx[0])
340+
})
341+
return el
342+
},
343+
),
340344
},
341345
)
342346
}).render()
343347

344348
expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
345349
})
346350

351+
test('dynamic slot props', async () => {
352+
let props: any
353+
354+
const bindObj = ref<Record<string, any>>({ foo: 1, baz: 'qux' })
355+
const Comp = defineComponent(() =>
356+
createSlot('default', [() => bindObj.value]),
357+
)
358+
define(() =>
359+
createComponent(
360+
Comp,
361+
{},
362+
{ default: _props => ((props = _props), []) },
363+
),
364+
).render()
365+
366+
expect(props).toEqual({ foo: 1, baz: 'qux' })
367+
368+
bindObj.value.foo = 2
369+
await nextTick()
370+
expect(props).toEqual({ foo: 2, baz: 'qux' })
371+
372+
delete bindObj.value.baz
373+
await nextTick()
374+
expect(props).toEqual({ foo: 2 })
375+
})
376+
377+
test('dynamic slot props with static slot props', async () => {
378+
let props: any
379+
380+
const foo = ref(0)
381+
const bindObj = ref<Record<string, any>>({ foo: 100, baz: 'qux' })
382+
const Comp = defineComponent(() =>
383+
createSlot('default', [{ foo: () => foo.value }, () => bindObj.value]),
384+
)
385+
define(() =>
386+
createComponent(
387+
Comp,
388+
{},
389+
{ default: _props => ((props = _props), []) },
390+
),
391+
).render()
392+
393+
expect(props).toEqual({ foo: 100, baz: 'qux' })
394+
395+
foo.value = 2
396+
await nextTick()
397+
expect(props).toEqual({ foo: 100, baz: 'qux' })
398+
399+
delete bindObj.value.foo
400+
await nextTick()
401+
expect(props).toEqual({ foo: 2, baz: 'qux' })
402+
})
403+
404+
test('slot class binding should be merged', async () => {
405+
let props: any
406+
407+
const className = ref('foo')
408+
const classObj = ref({ bar: true })
409+
const Comp = defineComponent(() =>
410+
createSlot('default', [
411+
{ class: () => className.value },
412+
() => ({ class: ['baz', 'qux'] }),
413+
{ class: () => classObj.value },
414+
]),
415+
)
416+
define(() =>
417+
createComponent(
418+
Comp,
419+
{},
420+
{ default: _props => ((props = _props), []) },
421+
),
422+
).render()
423+
424+
expect(props).toEqual({ class: 'foo baz qux bar' })
425+
426+
classObj.value.bar = false
427+
await nextTick()
428+
expect(props).toEqual({ class: 'foo baz qux' })
429+
430+
className.value = ''
431+
await nextTick()
432+
expect(props).toEqual({ class: 'baz qux' })
433+
})
434+
435+
test('slot style binding should be merged', async () => {
436+
let props: any
437+
438+
const style = ref<any>({ fontSize: '12px' })
439+
const Comp = defineComponent(() =>
440+
createSlot('default', [
441+
{ style: () => style.value },
442+
() => ({ style: { width: '100px', color: 'blue' } }),
443+
{ style: () => 'color: red' },
444+
]),
445+
)
446+
define(() =>
447+
createComponent(
448+
Comp,
449+
{},
450+
{ default: _props => ((props = _props), []) },
451+
),
452+
).render()
453+
454+
expect(props).toEqual({
455+
style: {
456+
fontSize: '12px',
457+
width: '100px',
458+
color: 'red',
459+
},
460+
})
461+
462+
style.value = null
463+
await nextTick()
464+
expect(props).toEqual({
465+
style: {
466+
width: '100px',
467+
color: 'red',
468+
},
469+
})
470+
})
471+
347472
test('dynamic slot should be render correctly with binds', async () => {
348473
const Comp = defineComponent(() => {
349474
const n0 = template('<div></div>')()
350475
prepend(
351476
n0 as any as ParentNode,
352-
createSlot('header', { title: () => 'header' }),
477+
createSlot('header', [{ title: () => 'header' }]),
353478
)
354479
return n0
355480
})
@@ -359,7 +484,7 @@ describe('component: slots', () => {
359484
return createComponent(Comp, {}, {}, [
360485
() => ({
361486
name: 'header',
362-
fn: ({ title }) => template(`${title()}`)(),
487+
fn: props => template(props.title)(),
363488
}),
364489
])
365490
}).render()
@@ -374,7 +499,7 @@ describe('component: slots', () => {
374499
n0 as any as ParentNode,
375500
createSlot(
376501
() => 'header', // dynamic slot outlet name
377-
{ title: () => 'header' },
502+
[{ title: () => 'header' }],
378503
),
379504
)
380505
return n0
@@ -384,7 +509,7 @@ describe('component: slots', () => {
384509
return createComponent(
385510
Comp,
386511
{},
387-
{ header: ({ title }) => template(`${title()}`)() },
512+
{ header: props => template(props.title)() },
388513
)
389514
}).render()
390515

@@ -395,7 +520,7 @@ describe('component: slots', () => {
395520
const Comp = defineComponent(() => {
396521
const n0 = template('<div></div>')()
397522
insert(
398-
createSlot('header', {}, () => template('fallback')()),
523+
createSlot('header', undefined, () => template('fallback')()),
399524
n0 as any as ParentNode,
400525
)
401526
return n0
@@ -415,8 +540,8 @@ describe('component: slots', () => {
415540
const temp0 = template('<p></p>')
416541
const el0 = temp0()
417542
const el1 = temp0()
418-
const slot1 = createSlot('one', {}, () => template('one fallback')())
419-
const slot2 = createSlot('two', {}, () => template('two fallback')())
543+
const slot1 = createSlot('one', [], () => template('one fallback')())
544+
const slot2 = createSlot('two', [], () => template('two fallback')())
420545
insert(slot1, el0 as any as ParentNode)
421546
insert(slot2, el1 as any as ParentNode)
422547
return [el0, el1]
@@ -458,7 +583,7 @@ describe('component: slots', () => {
458583
const el0 = temp0()
459584
const slot1 = createSlot(
460585
() => slotOutletName.value,
461-
{},
586+
undefined,
462587
() => template('fallback')(),
463588
)
464589
insert(slot1, el0 as any as ParentNode)

packages/runtime-vapor/src/componentSlots.ts

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import { type Block, type Fragment, fragmentKey } from './apiRender'
1414
import { firstEffect, renderEffect } from './renderEffect'
1515
import { createComment, createTextNode, insert, remove } from './dom/element'
1616
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
17+
import type { NormalizedRawProps } from './componentProps'
18+
import type { Data } from '@vue/runtime-shared'
19+
import { mergeProps } from './dom/prop'
1720

1821
// TODO: SSR
1922

@@ -106,7 +109,7 @@ export function initSlots(
106109

107110
export function createSlot(
108111
name: string | (() => string),
109-
binds?: Record<string, (() => unknown) | undefined>,
112+
binds?: NormalizedRawProps,
110113
fallback?: () => Block,
111114
): Block {
112115
let block: Block | undefined
@@ -120,7 +123,7 @@ export function createSlot(
120123

121124
// When not using dynamic slots, simplify the process to improve performance
122125
if (!isDynamicName && !isReactive(slots)) {
123-
if ((branch = slots[name] || fallback)) {
126+
if ((branch = withProps(slots[name]) || fallback)) {
124127
return branch(binds)
125128
} else {
126129
return []
@@ -137,7 +140,7 @@ export function createSlot(
137140

138141
// TODO lifecycle hooks
139142
renderEffect(() => {
140-
if ((branch = getSlot() || fallback) !== oldBranch) {
143+
if ((branch = withProps(getSlot()) || fallback) !== oldBranch) {
141144
parent ||= anchor.parentNode
142145
if (block) {
143146
scope!.stop()
@@ -155,4 +158,62 @@ export function createSlot(
155158
})
156159

157160
return fragment
161+
162+
function withProps<T extends (p: any) => any>(fn?: T) {
163+
if (fn)
164+
return (binds?: NormalizedRawProps): ReturnType<T> =>
165+
fn(binds && normalizeSlotProps(binds))
166+
}
167+
}
168+
169+
function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
170+
const { length } = rawPropsList
171+
const mergings = length > 1 ? shallowReactive<Data[]>([]) : undefined
172+
const result = shallowReactive<Data>({})
173+
174+
for (let i = 0; i < length; i++) {
175+
const rawProps = rawPropsList[i]
176+
if (isFunction(rawProps)) {
177+
// dynamic props
178+
renderEffect(() => {
179+
const props = rawProps()
180+
if (mergings) {
181+
mergings[i] = props
182+
} else {
183+
setDynamicProps(props)
184+
}
185+
})
186+
} else {
187+
// static props
188+
const props = mergings
189+
? (mergings[i] = shallowReactive<Data>({}))
190+
: result
191+
for (const key in rawProps) {
192+
const valueSource = rawProps[key]
193+
renderEffect(() => {
194+
props[key] = valueSource()
195+
})
196+
}
197+
}
198+
}
199+
200+
if (mergings) {
201+
renderEffect(() => {
202+
setDynamicProps(mergeProps(...mergings))
203+
})
204+
}
205+
206+
return result
207+
208+
function setDynamicProps(props: Data) {
209+
const otherExistingKeys = new Set(Object.keys(result))
210+
for (const key in props) {
211+
result[key] = props[key]
212+
otherExistingKeys.delete(key)
213+
}
214+
// delete other stale props
215+
for (const key of otherExistingKeys) {
216+
delete result[key]
217+
}
218+
}
158219
}

packages/runtime-vapor/src/dom/prop.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ export function setDynamicProps(el: Element, ...args: any) {
154154
}
155155

156156
// TODO copied from runtime-core
157-
function mergeProps(...args: Data[]) {
157+
export function mergeProps(...args: Data[]) {
158158
const ret: Data = {}
159159
for (let i = 0; i < args.length; i++) {
160160
const toMerge = args[i]

0 commit comments

Comments
 (0)