describe('createSlot', () => {
test('slot should be render correctly', () => {
const Comp = defineComponent(() => {
- const n0 = template('<div></div>')()
+ const n0 = template('<div>')()
insert(createSlot('header'), n0 as any as ParentNode)
return n0
})
return createComponent(Comp, {}, {})
}).render()
- expect(host.innerHTML).toBe('<div>fallback</div>')
+ expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
})
test('dynamic slot should be updated correctly', async () => {
const slotOutletName = ref('one')
const Child = defineComponent(() => {
- const temp0 = template('<p></p>')
+ const temp0 = template('<p>')
const el0 = temp0()
const slot1 = createSlot(
() => slotOutletName.value,
expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
})
+
+ test('non-exist slot', async () => {
+ const Child = defineComponent(() => {
+ const el0 = template('<p>')()
+ const slot = createSlot('not-exist', undefined)
+ insert(slot, el0 as any as ParentNode)
+ return el0
+ })
+
+ const { host } = define(() => {
+ return createComponent(Child)
+ }).render()
+
+ expect(host.innerHTML).toBe('<p></p>')
+ })
})
})
import { renderEffect } from './renderEffect'
import { type Block, type Fragment, fragmentKey } from './apiRender'
-import { type EffectScope, effectScope } from '@vue/reactivity'
+import { type EffectScope, effectScope, shallowReactive } from '@vue/reactivity'
import { createComment, createTextNode, insert, remove } from './dom/element'
type BlockFn = () => Block
let newValue: any
let oldValue: any
let branch: BlockFn | undefined
- let parent: ParentNode | undefined | null
let block: Block | undefined
let scope: EffectScope | undefined
const anchor = __DEV__ ? createComment('if') : createTextNode()
- const fragment: Fragment = {
+ const fragment: Fragment = shallowReactive({
nodes: [],
anchor,
[fragmentKey]: true,
- }
+ })
// TODO: SSR
// if (isHydrating) {
function doIf() {
if ((newValue = !!condition()) !== oldValue) {
- parent ||= anchor.parentNode
+ const parent = anchor.parentNode
if (block) {
scope!.stop()
remove(block, parent!)
effectScope,
isReactive,
shallowReactive,
+ shallowRef,
} from '@vue/reactivity'
import {
type ComponentInternalInstance,
} from './component'
import { type Block, type Fragment, fragmentKey } from './apiRender'
import { firstEffect, renderEffect } from './renderEffect'
-import { createComment, createTextNode, insert, remove } from './dom/element'
+import {
+ createComment,
+ createTextNode,
+ insert,
+ normalizeBlock,
+ remove,
+} from './dom/element'
import type { NormalizedRawProps } from './componentProps'
import type { Data } from '@vue/runtime-shared'
import { mergeProps } from './dom/prop'
export function createSlot(
name: string | (() => string),
binds?: NormalizedRawProps,
- fallback?: () => Block,
+ fallback?: Slot,
): 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
+ const { slots } = currentInstance!
+
+ const slotBlock = shallowRef<Block>()
+ let slotBranch: Slot | undefined
+ let slotScope: EffectScope | undefined
+
+ let fallbackBlock: Block | undefined
+ let fallbackBranch: Slot | undefined
+ let fallbackScope: EffectScope | undefined
- // When not using dynamic slots, simplify the process to improve performance
- if (!isDynamicName && !isReactive(slots)) {
- if ((branch = withProps(slots[name]) || fallback)) {
- return branch(binds)
+ const normalizeBinds = binds && normalizeSlotProps(binds)
+
+ const isDynamicName = isFunction(name)
+ // fast path for static slots & without fallback
+ if (!isDynamicName && !isReactive(slots) && !fallback) {
+ if ((slotBranch = slots[name])) {
+ return slotBranch(normalizeBinds)
} else {
return []
}
}
- const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
const anchor = __DEV__ ? createComment('slot') : createTextNode()
const fragment: Fragment = {
nodes: [],
// TODO lifecycle hooks
renderEffect(() => {
- if ((branch = withProps(getSlot()) || fallback) !== oldBranch) {
- parent ||= anchor.parentNode
- if (block) {
- scope!.stop()
- remove(block, parent!)
+ const parent = anchor.parentNode
+
+ if (
+ !slotBlock.value || // not initied
+ fallbackScope || // in fallback slot
+ isValidBlock(slotBlock.value) // slot block is valid
+ ) {
+ renderSlot(parent)
+ } else {
+ renderFallback(parent)
+ }
+ })
+
+ return fragment
+
+ function renderSlot(parent: ParentNode | null) {
+ // from fallback to slot
+ const fromFallback = fallbackScope
+ if (fromFallback) {
+ // clean fallback slot
+ fallbackScope!.stop()
+ remove(fallbackBlock!, parent!)
+ fallbackScope = fallbackBlock = undefined
+ }
+
+ const slotName = isFunction(name) ? name() : name
+ const branch = slots[slotName]!
+
+ if (branch) {
+ // init slot scope and block or switch branch
+ if (!slotScope || slotBranch !== branch) {
+ // clean previous slot
+ if (slotScope && !fromFallback) {
+ slotScope.stop()
+ remove(slotBlock.value!, parent!)
+ }
+
+ slotBranch = branch
+ slotScope = effectScope()
+ slotBlock.value = slotScope.run(() => slotBranch!(normalizeBinds))
}
- if ((oldBranch = branch)) {
- scope = effectScope()
- fragment.nodes = block = scope.run(() => branch!(binds))!
- parent && insert(block, parent, anchor)
+
+ // if slot block is valid, render it
+ if (slotBlock.value && isValidBlock(slotBlock.value)) {
+ fragment.nodes = slotBlock.value
+ parent && insert(slotBlock.value, parent, anchor)
} else {
- scope = block = undefined
- fragment.nodes = []
+ renderFallback(parent)
}
+ } else {
+ renderFallback(parent)
}
- })
+ }
- return fragment
+ function renderFallback(parent: ParentNode | null) {
+ // if slot branch is initied, remove it from DOM, but keep the scope
+ if (slotBranch) {
+ remove(slotBlock.value!, parent!)
+ }
- function withProps<T extends (p: any) => any>(fn?: T) {
- if (fn)
- return (binds?: NormalizedRawProps): ReturnType<T> =>
- fn(binds && normalizeSlotProps(binds))
+ fallbackBranch ||= fallback
+ if (fallbackBranch) {
+ fallbackScope = effectScope()
+ fragment.nodes = fallbackBlock = fallbackScope.run(() =>
+ fallbackBranch!(normalizeBinds),
+ )!
+ parent && insert(fallbackBlock, parent, anchor)
+ } else {
+ fragment.nodes = []
+ }
}
}
}
}
}
+
+function isValidBlock(block: Block) {
+ return (
+ normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0
+ )
+}