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 {
|
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>')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue