Skip to content

Commit 6c0e8a8

Browse files
committed
wip(vapor): handle slot fallback when content changes
1 parent 4318129 commit 6c0e8a8

File tree

3 files changed

+99
-25
lines changed

3 files changed

+99
-25
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import {
44
createComponent,
55
createForSlots,
6+
createIf,
67
createSlot,
78
createVaporApp,
89
defineVaporComponent,
@@ -430,5 +431,42 @@ describe('component: slots', () => {
430431

431432
expect(host.innerHTML).toBe('<p><!--slot--></p>')
432433
})
434+
435+
test('use fallback when inner content changes', async () => {
436+
const Child = {
437+
setup() {
438+
return createSlot('default', null, () =>
439+
document.createTextNode('fallback'),
440+
)
441+
},
442+
}
443+
444+
const toggle = ref(true)
445+
446+
const { html } = define({
447+
setup() {
448+
return createComponent(Child, null, {
449+
default: () => {
450+
return createIf(
451+
() => toggle.value,
452+
() => {
453+
return document.createTextNode('content')
454+
},
455+
)
456+
},
457+
})
458+
},
459+
}).render()
460+
461+
expect(html()).toBe('content<!--if--><!--slot-->')
462+
463+
toggle.value = false
464+
await nextTick()
465+
expect(html()).toBe('fallback<!--if--><!--slot-->')
466+
467+
toggle.value = true
468+
await nextTick()
469+
expect(html()).toBe('content<!--if--><!--slot-->')
470+
})
433471
})
434472
})

packages/runtime-vapor/src/block.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class DynamicFragment extends Fragment {
3030
anchor: Node
3131
scope: EffectScope | undefined
3232
current?: BlockFn
33+
fallback?: BlockFn
3334

3435
constructor(anchorLabel?: string) {
3536
super([])
@@ -63,6 +64,15 @@ export class DynamicFragment extends Fragment {
6364
this.scope = undefined
6465
this.nodes = []
6566
}
67+
68+
if (this.fallback && !isValidBlock(this.nodes)) {
69+
parent && remove(this.nodes, parent)
70+
this.nodes =
71+
(this.scope || (this.scope = new EffectScope())).run(this.fallback) ||
72+
[]
73+
parent && insert(this.nodes, parent, this.anchor)
74+
}
75+
6676
resetTracking()
6777
}
6878
}
@@ -80,28 +90,17 @@ export function isBlock(val: NonNullable<unknown>): val is Block {
8090
)
8191
}
8292

83-
/*! #__NO_SIDE_EFFECTS__ */
84-
// TODO this should be optimized away
85-
export function normalizeBlock(block: Block): Node[] {
86-
const nodes: Node[] = []
93+
export function isValidBlock(block: Block): boolean {
8794
if (block instanceof Node) {
88-
nodes.push(block)
89-
} else if (isArray(block)) {
90-
block.forEach(child => nodes.push(...normalizeBlock(child)))
95+
return !(block instanceof Comment)
9196
} else if (isVaporComponent(block)) {
92-
nodes.push(...normalizeBlock(block.block!))
93-
} else if (block) {
94-
nodes.push(...normalizeBlock(block.nodes))
95-
block.anchor && nodes.push(block.anchor)
97+
return isValidBlock(block.block)
98+
} else if (isArray(block)) {
99+
return block.length > 0 && block.every(isValidBlock)
100+
} else {
101+
// fragment
102+
return isValidBlock(block.nodes)
96103
}
97-
return nodes
98-
}
99-
100-
// TODO optimize
101-
export function isValidBlock(block: Block): boolean {
102-
return (
103-
normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0
104-
)
105104
}
106105

107106
export function insert(
@@ -166,3 +165,26 @@ export function remove(block: Block, parent: ParentNode): void {
166165
parentsWithUnmountedChildren = null
167166
}
168167
}
168+
169+
/**
170+
* dev / test only
171+
*/
172+
export function normalizeBlock(block: Block): Node[] {
173+
if (!__DEV__ && !__TEST__) {
174+
throw new Error(
175+
'normalizeBlock should not be used in production code paths',
176+
)
177+
}
178+
const nodes: Node[] = []
179+
if (block instanceof Node) {
180+
nodes.push(block)
181+
} else if (isArray(block)) {
182+
block.forEach(child => nodes.push(...normalizeBlock(child)))
183+
} else if (isVaporComponent(block)) {
184+
nodes.push(...normalizeBlock(block.block!))
185+
} else {
186+
nodes.push(...normalizeBlock(block.nodes))
187+
block.anchor && nodes.push(block.anchor)
188+
}
189+
return nodes
190+
}

packages/runtime-vapor/src/componentSlots.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
5757
deleteProperty: NO,
5858
}
5959

60-
export function getSlot(target: RawSlots, key: string): Slot | undefined {
60+
export function getSlot(
61+
target: RawSlots,
62+
key: string,
63+
): (Slot & { _bound?: Slot }) | undefined {
6164
if (key === '$') return
6265
const dynamicSources = target.$
6366
if (dynamicSources) {
@@ -116,20 +119,31 @@ export function createSlot(
116119
? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers)
117120
: EMPTY_OBJ
118121

119-
const renderSlot = (name: string) => {
120-
const slot = getSlot(rawSlots, name)
122+
const renderSlot = () => {
123+
const slot = getSlot(rawSlots, isFunction(name) ? name() : name)
121124
if (slot) {
122-
fragment.update(() => slot(slotProps) || (fallback && fallback()))
125+
// create and cache bound version of the slot to make it stable
126+
// so that we avoid unnecessary updates if it resolves to the same slot
127+
fragment.update(
128+
slot._bound ||
129+
(slot._bound = () => {
130+
const slotContent = slot(slotProps)
131+
if (slotContent instanceof DynamicFragment) {
132+
slotContent.fallback = fallback
133+
}
134+
return slotContent
135+
}),
136+
)
123137
} else {
124138
fragment.update(fallback)
125139
}
126140
}
127141

128142
// dynamic slot name or has dynamicSlots
129143
if (isDynamicName || rawSlots.$) {
130-
renderEffect(() => renderSlot(isFunction(name) ? name() : name))
144+
renderEffect(renderSlot)
131145
} else {
132-
renderSlot(name)
146+
renderSlot()
133147
}
134148

135149
return fragment

0 commit comments

Comments
 (0)