feat(runtime-vapor): `createSlot` (#170)

This commit is contained in:
Rizumu Ayaka 2024-04-14 17:41:58 +08:00 committed by GitHub
parent a0bd0e9c5f
commit 07926564fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 312 additions and 46 deletions

View File

@ -2,11 +2,16 @@
import { import {
createComponent, createComponent,
createSlot,
createVaporApp, createVaporApp,
defineComponent, defineComponent,
getCurrentInstance, getCurrentInstance,
insert,
nextTick, nextTick,
prepend,
ref, ref,
renderEffect,
setText,
template, template,
} from '../src' } from '../src'
import { makeRender } from './_utils' import { makeRender } from './_utils'
@ -237,4 +242,190 @@ describe('component: slots', () => {
'Slot "default" invoked outside of the render function', 'Slot "default" invoked outside of the render function',
).not.toHaveBeenWarned() ).not.toHaveBeenWarned()
}) })
describe('createSlot', () => {
test('slot should be render correctly', () => {
const Comp = defineComponent(() => {
const n0 = template('<div></div>')()
insert(createSlot('header'), n0 as any as ParentNode)
return n0
})
const { host } = define(() => {
return createComponent(Comp, {}, { header: () => template('header')() })
}).render()
expect(host.innerHTML).toBe('<div>header</div>')
})
test('slot should be render correctly with binds', async () => {
const Comp = defineComponent(() => {
const n0 = template('<div></div>')()
insert(
createSlot('header', { title: () => 'header' }),
n0 as any as ParentNode,
)
return n0
})
const { host } = define(() => {
return createComponent(
Comp,
{},
{
header: ({ title }) => {
const el = template('<h1></h1>')()
renderEffect(() => {
setText(el, title())
})
return el
},
},
)
}).render()
expect(host.innerHTML).toBe('<div><h1>header</h1></div>')
})
test('dynamic slot should be render correctly with binds', async () => {
const Comp = defineComponent(() => {
const n0 = template('<div></div>')()
prepend(
n0 as any as ParentNode,
createSlot('header', { title: () => 'header' }),
)
return n0
})
const { host } = define(() => {
// dynamic slot
return createComponent(Comp, {}, {}, () => [
{ name: 'header', fn: ({ title }) => template(`${title()}`)() },
])
}).render()
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
})
test('dynamic slot outlet should be render correctly with binds', async () => {
const Comp = defineComponent(() => {
const n0 = template('<div></div>')()
prepend(
n0 as any as ParentNode,
createSlot(
() => 'header', // dynamic slot outlet name
{ title: () => 'header' },
),
)
return n0
})
const { host } = define(() => {
return createComponent(
Comp,
{},
{ header: ({ title }) => template(`${title()}`)() },
)
}).render()
expect(host.innerHTML).toBe('<div>header<!--slot--></div>')
})
test('fallback should be render correctly', () => {
const Comp = defineComponent(() => {
const n0 = template('<div></div>')()
insert(
createSlot('header', {}, () => template('fallback')()),
n0 as any as ParentNode,
)
return n0
})
const { host } = define(() => {
return createComponent(Comp, {}, {})
}).render()
expect(host.innerHTML).toBe('<div>fallback</div>')
})
test('dynamic slot should be updated correctly', async () => {
const flag1 = ref(true)
const Child = defineComponent(() => {
const temp0 = template('<p></p>')
const el0 = temp0()
const el1 = temp0()
const slot1 = createSlot('one', {}, () => template('one fallback')())
const slot2 = createSlot('two', {}, () => template('two fallback')())
insert(slot1, el0 as any as ParentNode)
insert(slot2, el1 as any as ParentNode)
return [el0, el1]
})
const { host } = define(() => {
return createComponent(Child, {}, {}, () => [
flag1.value
? { name: 'one', fn: () => template('one content')() }
: { name: 'two', fn: () => template('two content')() },
])
}).render()
expect(host.innerHTML).toBe(
'<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
)
flag1.value = false
await nextTick()
expect(host.innerHTML).toBe(
'<p>one fallback<!--slot--></p><p>two content<!--slot--></p>',
)
flag1.value = true
await nextTick()
expect(host.innerHTML).toBe(
'<p>one content<!--slot--></p><p>two fallback<!--slot--></p>',
)
})
test('dynamic slot outlet should be updated correctly', async () => {
const slotOutletName = ref('one')
const Child = defineComponent(() => {
const temp0 = template('<p></p>')
const el0 = temp0()
const slot1 = createSlot(
() => slotOutletName.value,
{},
() => template('fallback')(),
)
insert(slot1, el0 as any as ParentNode)
return el0
})
const { host } = define(() => {
return createComponent(
Child,
{},
{
one: () => template('one content')(),
two: () => template('two content')(),
},
)
}).render()
expect(host.innerHTML).toBe('<p>one content<!--slot--></p>')
slotOutletName.value = 'two'
await nextTick()
expect(host.innerHTML).toBe('<p>two content<!--slot--></p>')
slotOutletName.value = 'none'
await nextTick()
expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
})
})
}) })

View File

@ -1,8 +1,23 @@
import { type IfAny, isArray } from '@vue/shared' import { type IfAny, isArray, isFunction } from '@vue/shared'
import { baseWatch } from '@vue/reactivity' import {
import { type ComponentInternalInstance, setCurrentInstance } from './component' type EffectScope,
import type { Block } from './apiRender' ReactiveEffect,
import { createVaporPreScheduler } from './scheduler' type SchedulerJob,
SchedulerJobFlags,
effectScope,
isReactive,
shallowReactive,
} from '@vue/reactivity'
import {
type ComponentInternalInstance,
currentInstance,
setCurrentInstance,
} from './component'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { renderEffect } from './renderEffect'
import { createComment, createTextNode, insert, remove } from './dom/element'
import { queueJob } from './scheduler'
import { VaporErrorCodes, callWithAsyncErrorHandling } from './errorHandling'
// TODO: SSR // TODO: SSR
@ -29,7 +44,7 @@ export const initSlots = (
rawSlots: InternalSlots | null = null, rawSlots: InternalSlots | null = null,
dynamicSlots: DynamicSlots | null = null, dynamicSlots: DynamicSlots | null = null,
) => { ) => {
const slots: InternalSlots = {} let slots: InternalSlots = {}
for (const key in rawSlots) { for (const key in rawSlots) {
const slot = rawSlots[key] const slot = rawSlots[key]
@ -39,10 +54,15 @@ export const initSlots = (
} }
if (dynamicSlots) { if (dynamicSlots) {
slots = shallowReactive(slots)
const dynamicSlotKeys: Record<string, true> = {} const dynamicSlotKeys: Record<string, true> = {}
baseWatch(
() => { const effect = new ReactiveEffect(() => {
const _dynamicSlots = dynamicSlots() const _dynamicSlots = callWithAsyncErrorHandling(
dynamicSlots,
instance,
VaporErrorCodes.RENDER_FUNCTION,
)
for (let i = 0; i < _dynamicSlots.length; i++) { for (let i = 0; i < _dynamicSlots.length; i++) {
const slot = _dynamicSlots[i] const slot = _dynamicSlots[i]
// array of dynamic slot generated by <template v-for="..." #[...]> // array of dynamic slot generated by <template v-for="..." #[...]>
@ -71,18 +91,19 @@ export const initSlots = (
for (const key in dynamicSlotKeys) { for (const key in dynamicSlotKeys) {
if ( if (
!_dynamicSlots.some(slot => !_dynamicSlots.some(slot =>
isArray(slot) isArray(slot) ? slot.some(s => s.name === key) : slot?.name === key,
? slot.some(s => s.name === key)
: slot?.name === key,
) )
) { ) {
delete slots[key] delete slots[key]
} }
} }
}, })
undefined,
{ scheduler: createVaporPreScheduler(instance) }, const job: SchedulerJob = () => effect.run()
) job.flags! |= SchedulerJobFlags.PRE
job.id = instance.uid
effect.scheduler = () => queueJob(job)
effect.run()
} }
instance.slots = slots instance.slots = slots
@ -98,3 +119,56 @@ export const initSlots = (
} }
} }
} }
export function createSlot(
name: string | (() => string),
binds?: Record<string, (() => unknown) | undefined>,
fallback?: () => Block,
): Block {
let block: Block | undefined
let branch: Slot | undefined
let oldBranch: Slot | undefined
let parent: ParentNode | undefined | null
let scope: EffectScope | undefined
const isDynamicName = isFunction(name)
const instance = currentInstance!
const { slots } = instance
// When not using dynamic slots, simplify the process to improve performance
if (!isDynamicName && !isReactive(slots)) {
if ((branch = slots[name] || fallback)) {
return branch(binds)
} else {
return []
}
}
const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
const anchor = __DEV__ ? createComment('slot') : createTextNode()
const fragment: Fragment = {
nodes: [],
anchor,
[fragmentKey]: true,
}
// TODO lifecycle hooks
renderEffect(() => {
if ((branch = getSlot() || fallback) !== oldBranch) {
parent ||= anchor.parentNode
if (block) {
scope!.stop()
remove(block, parent!)
}
if ((oldBranch = branch)) {
scope = effectScope()
fragment.nodes = block = scope.run(() => branch!(binds))!
parent && insert(block, parent, anchor)
} else {
scope = block = undefined
fragment.nodes = []
}
}
})
return fragment
}

View File

@ -50,6 +50,7 @@ export {
type FunctionalComponent, type FunctionalComponent,
type SetupFn, type SetupFn,
} from './component' } from './component'
export { createSlot } from './componentSlots'
export { renderEffect } from './renderEffect' export { renderEffect } from './renderEffect'
export { export {
watch, watch,