mirror of https://github.com/vuejs/core.git
feat(runtime-vapor): `createSlot` (#170)
This commit is contained in:
parent
a0bd0e9c5f
commit
07926564fc
|
@ -2,11 +2,16 @@
|
|||
|
||||
import {
|
||||
createComponent,
|
||||
createSlot,
|
||||
createVaporApp,
|
||||
defineComponent,
|
||||
getCurrentInstance,
|
||||
insert,
|
||||
nextTick,
|
||||
prepend,
|
||||
ref,
|
||||
renderEffect,
|
||||
setText,
|
||||
template,
|
||||
} from '../src'
|
||||
import { makeRender } from './_utils'
|
||||
|
@ -237,4 +242,190 @@ describe('component: slots', () => {
|
|||
'Slot "default" invoked outside of the render function',
|
||||
).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>')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,8 +1,23 @@
|
|||
import { type IfAny, isArray } from '@vue/shared'
|
||||
import { baseWatch } from '@vue/reactivity'
|
||||
import { type ComponentInternalInstance, setCurrentInstance } from './component'
|
||||
import type { Block } from './apiRender'
|
||||
import { createVaporPreScheduler } from './scheduler'
|
||||
import { type IfAny, isArray, isFunction } from '@vue/shared'
|
||||
import {
|
||||
type EffectScope,
|
||||
ReactiveEffect,
|
||||
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
|
||||
|
||||
|
@ -29,7 +44,7 @@ export const initSlots = (
|
|||
rawSlots: InternalSlots | null = null,
|
||||
dynamicSlots: DynamicSlots | null = null,
|
||||
) => {
|
||||
const slots: InternalSlots = {}
|
||||
let slots: InternalSlots = {}
|
||||
|
||||
for (const key in rawSlots) {
|
||||
const slot = rawSlots[key]
|
||||
|
@ -39,50 +54,56 @@ export const initSlots = (
|
|||
}
|
||||
|
||||
if (dynamicSlots) {
|
||||
slots = shallowReactive(slots)
|
||||
const dynamicSlotKeys: Record<string, true> = {}
|
||||
baseWatch(
|
||||
() => {
|
||||
const _dynamicSlots = dynamicSlots()
|
||||
for (let i = 0; i < _dynamicSlots.length; i++) {
|
||||
const slot = _dynamicSlots[i]
|
||||
// array of dynamic slot generated by <template v-for="..." #[...]>
|
||||
if (isArray(slot)) {
|
||||
for (let j = 0; j < slot.length; j++) {
|
||||
slots[slot[j].name] = withCtx(slot[j].fn)
|
||||
dynamicSlotKeys[slot[j].name] = true
|
||||
}
|
||||
} else if (slot) {
|
||||
// conditional single slot generated by <template v-if="..." #foo>
|
||||
slots[slot.name] = withCtx(
|
||||
slot.key
|
||||
? (...args: any[]) => {
|
||||
const res = slot.fn(...args)
|
||||
// attach branch key so each conditional branch is considered a
|
||||
// different fragment
|
||||
if (res) (res as any).key = slot.key
|
||||
return res
|
||||
}
|
||||
: slot.fn,
|
||||
)
|
||||
dynamicSlotKeys[slot.name] = true
|
||||
|
||||
const effect = new ReactiveEffect(() => {
|
||||
const _dynamicSlots = callWithAsyncErrorHandling(
|
||||
dynamicSlots,
|
||||
instance,
|
||||
VaporErrorCodes.RENDER_FUNCTION,
|
||||
)
|
||||
for (let i = 0; i < _dynamicSlots.length; i++) {
|
||||
const slot = _dynamicSlots[i]
|
||||
// array of dynamic slot generated by <template v-for="..." #[...]>
|
||||
if (isArray(slot)) {
|
||||
for (let j = 0; j < slot.length; j++) {
|
||||
slots[slot[j].name] = withCtx(slot[j].fn)
|
||||
dynamicSlotKeys[slot[j].name] = true
|
||||
}
|
||||
} else if (slot) {
|
||||
// conditional single slot generated by <template v-if="..." #foo>
|
||||
slots[slot.name] = withCtx(
|
||||
slot.key
|
||||
? (...args: any[]) => {
|
||||
const res = slot.fn(...args)
|
||||
// attach branch key so each conditional branch is considered a
|
||||
// different fragment
|
||||
if (res) (res as any).key = slot.key
|
||||
return res
|
||||
}
|
||||
: slot.fn,
|
||||
)
|
||||
dynamicSlotKeys[slot.name] = true
|
||||
}
|
||||
// delete stale slots
|
||||
for (const key in dynamicSlotKeys) {
|
||||
if (
|
||||
!_dynamicSlots.some(slot =>
|
||||
isArray(slot)
|
||||
? slot.some(s => s.name === key)
|
||||
: slot?.name === key,
|
||||
)
|
||||
) {
|
||||
delete slots[key]
|
||||
}
|
||||
}
|
||||
// delete stale slots
|
||||
for (const key in dynamicSlotKeys) {
|
||||
if (
|
||||
!_dynamicSlots.some(slot =>
|
||||
isArray(slot) ? slot.some(s => s.name === key) : slot?.name === 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
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export {
|
|||
type FunctionalComponent,
|
||||
type SetupFn,
|
||||
} from './component'
|
||||
export { createSlot } from './componentSlots'
|
||||
export { renderEffect } from './renderEffect'
|
||||
export {
|
||||
watch,
|
||||
|
|
Loading…
Reference in New Issue