Skip to content
This repository was archived by the owner on Jul 19, 2025. It is now read-only.

Commit c223eb2

Browse files
committed
fix(runtime-vapor): switch to fallback when slot is empty
1 parent 7f3ca46 commit c223eb2

File tree

4 files changed

+118
-41
lines changed

4 files changed

+118
-41
lines changed

packages/runtime-vapor/__tests__/componentSlots.spec.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ describe('component: slots', () => {
365365
describe('createSlot', () => {
366366
test('slot should be render correctly', () => {
367367
const Comp = defineComponent(() => {
368-
const n0 = template('<div></div>')()
368+
const n0 = template('<div>')()
369369
insert(createSlot('header'), n0 as any as ParentNode)
370370
return n0
371371
})
@@ -589,7 +589,7 @@ describe('component: slots', () => {
589589
return createComponent(Comp, {}, {})
590590
}).render()
591591

592-
expect(host.innerHTML).toBe('<div>fallback</div>')
592+
expect(host.innerHTML).toBe('<div>fallback<!--slot--></div>')
593593
})
594594

595595
test('dynamic slot should be updated correctly', async () => {
@@ -638,7 +638,7 @@ describe('component: slots', () => {
638638
const slotOutletName = ref('one')
639639

640640
const Child = defineComponent(() => {
641-
const temp0 = template('<p></p>')
641+
const temp0 = template('<p>')
642642
const el0 = temp0()
643643
const slot1 = createSlot(
644644
() => slotOutletName.value,
@@ -672,5 +672,20 @@ describe('component: slots', () => {
672672

673673
expect(host.innerHTML).toBe('<p>fallback<!--slot--></p>')
674674
})
675+
676+
test('non-exist slot', async () => {
677+
const Child = defineComponent(() => {
678+
const el0 = template('<p>')()
679+
const slot = createSlot('not-exist', undefined)
680+
insert(slot, el0 as any as ParentNode)
681+
return el0
682+
})
683+
684+
const { host } = define(() => {
685+
return createComponent(Child)
686+
}).render()
687+
688+
expect(host.innerHTML).toBe('<p></p>')
689+
})
675690
})
676691
})

packages/runtime-vapor/src/apiCreateIf.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { renderEffect } from './renderEffect'
22
import { type Block, type Fragment, fragmentKey } from './apiRender'
3-
import { type EffectScope, effectScope } from '@vue/reactivity'
3+
import { type EffectScope, effectScope, shallowReactive } from '@vue/reactivity'
44
import { createComment, createTextNode, insert, remove } from './dom/element'
55

66
type BlockFn = () => Block
@@ -16,15 +16,14 @@ export const createIf = (
1616
let newValue: any
1717
let oldValue: any
1818
let branch: BlockFn | undefined
19-
let parent: ParentNode | undefined | null
2019
let block: Block | undefined
2120
let scope: EffectScope | undefined
2221
const anchor = __DEV__ ? createComment('if') : createTextNode()
23-
const fragment: Fragment = {
22+
const fragment: Fragment = shallowReactive({
2423
nodes: [],
2524
anchor,
2625
[fragmentKey]: true,
27-
}
26+
})
2827

2928
// TODO: SSR
3029
// if (isHydrating) {
@@ -47,7 +46,7 @@ export const createIf = (
4746

4847
function doIf() {
4948
if ((newValue = !!condition()) !== oldValue) {
50-
parent ||= anchor.parentNode
49+
const parent = anchor.parentNode
5150
if (block) {
5251
scope!.stop()
5352
remove(block, parent!)

packages/runtime-vapor/src/componentSlots.ts

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
effectScope,
55
isReactive,
66
shallowReactive,
7+
shallowRef,
78
} from '@vue/reactivity'
89
import {
910
type ComponentInternalInstance,
@@ -12,7 +13,13 @@ import {
1213
} from './component'
1314
import { type Block, type Fragment, fragmentKey } from './apiRender'
1415
import { firstEffect, renderEffect } from './renderEffect'
15-
import { createComment, createTextNode, insert, remove } from './dom/element'
16+
import {
17+
createComment,
18+
createTextNode,
19+
insert,
20+
normalizeBlock,
21+
remove,
22+
} from './dom/element'
1623
import type { NormalizedRawProps } from './componentProps'
1724
import type { Data } from '@vue/runtime-shared'
1825
import { mergeProps } from './dom/prop'
@@ -107,27 +114,30 @@ export function initSlots(
107114
export function createSlot(
108115
name: string | (() => string),
109116
binds?: NormalizedRawProps,
110-
fallback?: () => Block,
117+
fallback?: Slot,
111118
): Block {
112-
let block: Block | undefined
113-
let branch: Slot | undefined
114-
let oldBranch: Slot | undefined
115-
let parent: ParentNode | undefined | null
116-
let scope: EffectScope | undefined
117-
const isDynamicName = isFunction(name)
118-
const instance = currentInstance!
119-
const { slots } = instance
119+
const { slots } = currentInstance!
120+
121+
const slotBlock = shallowRef<Block>()
122+
let slotBranch: Slot | undefined
123+
let slotScope: EffectScope | undefined
124+
125+
let fallbackBlock: Block | undefined
126+
let fallbackBranch: Slot | undefined
127+
let fallbackScope: EffectScope | undefined
120128

121-
// When not using dynamic slots, simplify the process to improve performance
122-
if (!isDynamicName && !isReactive(slots)) {
123-
if ((branch = withProps(slots[name]) || fallback)) {
124-
return branch(binds)
129+
const normalizeBinds = binds && normalizeSlotProps(binds)
130+
131+
const isDynamicName = isFunction(name)
132+
// fast path for static slots & without fallback
133+
if (!isDynamicName && !isReactive(slots) && !fallback) {
134+
if ((slotBranch = slots[name])) {
135+
return slotBranch(normalizeBinds)
125136
} else {
126137
return []
127138
}
128139
}
129140

130-
const getSlot = isDynamicName ? () => slots[name()] : () => slots[name]
131141
const anchor = __DEV__ ? createComment('slot') : createTextNode()
132142
const fragment: Fragment = {
133143
nodes: [],
@@ -137,29 +147,76 @@ export function createSlot(
137147

138148
// TODO lifecycle hooks
139149
renderEffect(() => {
140-
if ((branch = withProps(getSlot()) || fallback) !== oldBranch) {
141-
parent ||= anchor.parentNode
142-
if (block) {
143-
scope!.stop()
144-
remove(block, parent!)
150+
const parent = anchor.parentNode
151+
152+
if (
153+
!slotBlock.value || // not initied
154+
fallbackScope || // in fallback slot
155+
isValidBlock(slotBlock.value) // slot block is valid
156+
) {
157+
renderSlot(parent)
158+
} else {
159+
renderFallback(parent)
160+
}
161+
})
162+
163+
return fragment
164+
165+
function renderSlot(parent: ParentNode | null) {
166+
// from fallback to slot
167+
const fromFallback = fallbackScope
168+
if (fromFallback) {
169+
// clean fallback slot
170+
fallbackScope!.stop()
171+
remove(fallbackBlock!, parent!)
172+
fallbackScope = fallbackBlock = undefined
173+
}
174+
175+
const slotName = isFunction(name) ? name() : name
176+
const branch = slots[slotName]!
177+
178+
if (branch) {
179+
// init slot scope and block or switch branch
180+
if (!slotScope || slotBranch !== branch) {
181+
// clean previous slot
182+
if (slotScope && !fromFallback) {
183+
slotScope.stop()
184+
remove(slotBlock.value!, parent!)
185+
}
186+
187+
slotBranch = branch
188+
slotScope = effectScope()
189+
slotBlock.value = slotScope.run(() => slotBranch!(normalizeBinds))
145190
}
146-
if ((oldBranch = branch)) {
147-
scope = effectScope()
148-
fragment.nodes = block = scope.run(() => branch!(binds))!
149-
parent && insert(block, parent, anchor)
191+
192+
// if slot block is valid, render it
193+
if (slotBlock.value && isValidBlock(slotBlock.value)) {
194+
fragment.nodes = slotBlock.value
195+
parent && insert(slotBlock.value, parent, anchor)
150196
} else {
151-
scope = block = undefined
152-
fragment.nodes = []
197+
renderFallback(parent)
153198
}
199+
} else {
200+
renderFallback(parent)
154201
}
155-
})
202+
}
156203

157-
return fragment
204+
function renderFallback(parent: ParentNode | null) {
205+
// if slot branch is initied, remove it from DOM, but keep the scope
206+
if (slotBranch) {
207+
remove(slotBlock.value!, parent!)
208+
}
158209

159-
function withProps<T extends (p: any) => any>(fn?: T) {
160-
if (fn)
161-
return (binds?: NormalizedRawProps): ReturnType<T> =>
162-
fn(binds && normalizeSlotProps(binds))
210+
fallbackBranch ||= fallback
211+
if (fallbackBranch) {
212+
fallbackScope = effectScope()
213+
fragment.nodes = fallbackBlock = fallbackScope.run(() =>
214+
fallbackBranch!(normalizeBinds),
215+
)!
216+
parent && insert(fallbackBlock, parent, anchor)
217+
} else {
218+
fragment.nodes = []
219+
}
163220
}
164221
}
165222

@@ -214,3 +271,9 @@ function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
214271
}
215272
}
216273
}
274+
275+
function isValidBlock(block: Block) {
276+
return (
277+
normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0
278+
)
279+
}

playground/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { createVaporApp } from 'vue/vapor'
44
import { createApp } from 'vue'
55
import './style.css'
66

7-
const modules = import.meta.glob<any>('./**/*.(vue|js)')
7+
const modules = import.meta.glob<any>('./**/*.(vue|js|ts)')
88
const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
99

1010
mod.then(({ default: mod }) => {

0 commit comments

Comments
 (0)