diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
index c0e1b716f..d769c0f19 100644
--- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts
@@ -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('
')()
+ insert(createSlot('header'), n0 as any as ParentNode)
+ return n0
+ })
+
+ const { host } = define(() => {
+ return createComponent(Comp, {}, { header: () => template('header')() })
+ }).render()
+
+ expect(host.innerHTML).toBe('header
')
+ })
+
+ test('slot should be render correctly with binds', async () => {
+ const Comp = defineComponent(() => {
+ const n0 = template('')()
+ insert(
+ createSlot('header', { title: () => 'header' }),
+ n0 as any as ParentNode,
+ )
+ return n0
+ })
+
+ const { host } = define(() => {
+ return createComponent(
+ Comp,
+ {},
+ {
+ header: ({ title }) => {
+ const el = template('')()
+ renderEffect(() => {
+ setText(el, title())
+ })
+ return el
+ },
+ },
+ )
+ }).render()
+
+ expect(host.innerHTML).toBe('header
')
+ })
+
+ test('dynamic slot should be render correctly with binds', async () => {
+ const Comp = defineComponent(() => {
+ const n0 = template('')()
+ 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('header
')
+ })
+
+ test('dynamic slot outlet should be render correctly with binds', async () => {
+ const Comp = defineComponent(() => {
+ const n0 = template('')()
+ 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('header
')
+ })
+
+ test('fallback should be render correctly', () => {
+ const Comp = defineComponent(() => {
+ const n0 = template('')()
+ insert(
+ createSlot('header', {}, () => template('fallback')()),
+ n0 as any as ParentNode,
+ )
+ return n0
+ })
+
+ const { host } = define(() => {
+ return createComponent(Comp, {}, {})
+ }).render()
+
+ expect(host.innerHTML).toBe('fallback
')
+ })
+
+ test('dynamic slot should be updated correctly', async () => {
+ const flag1 = ref(true)
+
+ const Child = defineComponent(() => {
+ const temp0 = template('')
+ 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(
+ 'one content
two fallback
',
+ )
+
+ flag1.value = false
+ await nextTick()
+
+ expect(host.innerHTML).toBe(
+ 'one fallback
two content
',
+ )
+
+ flag1.value = true
+ await nextTick()
+
+ expect(host.innerHTML).toBe(
+ 'one content
two fallback
',
+ )
+ })
+
+ test('dynamic slot outlet should be updated correctly', async () => {
+ const slotOutletName = ref('one')
+
+ const Child = defineComponent(() => {
+ const temp0 = template('')
+ 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('one content
')
+
+ slotOutletName.value = 'two'
+ await nextTick()
+
+ expect(host.innerHTML).toBe('two content
')
+
+ slotOutletName.value = 'none'
+ await nextTick()
+
+ expect(host.innerHTML).toBe('fallback
')
+ })
+ })
})
diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts
index 48ea4509c..dc2da78ea 100644
--- a/packages/runtime-vapor/src/componentSlots.ts
+++ b/packages/runtime-vapor/src/componentSlots.ts
@@ -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 = {}
- baseWatch(
- () => {
- const _dynamicSlots = dynamicSlots()
- for (let i = 0; i < _dynamicSlots.length; i++) {
- const slot = _dynamicSlots[i]
- // array of dynamic slot generated by
- 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
- 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
+ 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
+ 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 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
+}
diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts
index b15f4c461..919e0c2c6 100644
--- a/packages/runtime-vapor/src/index.ts
+++ b/packages/runtime-vapor/src/index.ts
@@ -50,6 +50,7 @@ export {
type FunctionalComponent,
type SetupFn,
} from './component'
+export { createSlot } from './componentSlots'
export { renderEffect } from './renderEffect'
export {
watch,