From: Evan You Date: Sat, 14 Dec 2024 14:15:34 +0000 (+0800) Subject: wip(vapor): handle slot fallback when content changes X-Git-Tag: v3.6.0-alpha.1~16^2~145 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6c0e8a8f242e46055f645e76892fa42e88aa9903;p=thirdparty%2Fvuejs%2Fcore.git wip(vapor): handle slot fallback when content changes --- diff --git a/packages/runtime-vapor/__tests__/componentSlots.spec.ts b/packages/runtime-vapor/__tests__/componentSlots.spec.ts index 868e45671c..f4ff0c31cb 100644 --- a/packages/runtime-vapor/__tests__/componentSlots.spec.ts +++ b/packages/runtime-vapor/__tests__/componentSlots.spec.ts @@ -3,6 +3,7 @@ import { createComponent, createForSlots, + createIf, createSlot, createVaporApp, defineVaporComponent, @@ -430,5 +431,42 @@ describe('component: slots', () => { expect(host.innerHTML).toBe('

') }) + + test('use fallback when inner content changes', async () => { + const Child = { + setup() { + return createSlot('default', null, () => + document.createTextNode('fallback'), + ) + }, + } + + const toggle = ref(true) + + const { html } = define({ + setup() { + return createComponent(Child, null, { + default: () => { + return createIf( + () => toggle.value, + () => { + return document.createTextNode('content') + }, + ) + }, + }) + }, + }).render() + + expect(html()).toBe('content') + + toggle.value = false + await nextTick() + expect(html()).toBe('fallback') + + toggle.value = true + await nextTick() + expect(html()).toBe('content') + }) }) }) diff --git a/packages/runtime-vapor/src/block.ts b/packages/runtime-vapor/src/block.ts index eccd573322..d1daa8660d 100644 --- a/packages/runtime-vapor/src/block.ts +++ b/packages/runtime-vapor/src/block.ts @@ -30,6 +30,7 @@ export class DynamicFragment extends Fragment { anchor: Node scope: EffectScope | undefined current?: BlockFn + fallback?: BlockFn constructor(anchorLabel?: string) { super([]) @@ -63,6 +64,15 @@ export class DynamicFragment extends Fragment { this.scope = undefined this.nodes = [] } + + if (this.fallback && !isValidBlock(this.nodes)) { + parent && remove(this.nodes, parent) + this.nodes = + (this.scope || (this.scope = new EffectScope())).run(this.fallback) || + [] + parent && insert(this.nodes, parent, this.anchor) + } + resetTracking() } } @@ -80,28 +90,17 @@ export function isBlock(val: NonNullable): val is Block { ) } -/*! #__NO_SIDE_EFFECTS__ */ -// TODO this should be optimized away -export function normalizeBlock(block: Block): Node[] { - const nodes: Node[] = [] +export function isValidBlock(block: Block): boolean { if (block instanceof Node) { - nodes.push(block) - } else if (isArray(block)) { - block.forEach(child => nodes.push(...normalizeBlock(child))) + return !(block instanceof Comment) } else if (isVaporComponent(block)) { - nodes.push(...normalizeBlock(block.block!)) - } else if (block) { - nodes.push(...normalizeBlock(block.nodes)) - block.anchor && nodes.push(block.anchor) + return isValidBlock(block.block) + } else if (isArray(block)) { + return block.length > 0 && block.every(isValidBlock) + } else { + // fragment + return isValidBlock(block.nodes) } - return nodes -} - -// TODO optimize -export function isValidBlock(block: Block): boolean { - return ( - normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0 - ) } export function insert( @@ -166,3 +165,26 @@ export function remove(block: Block, parent: ParentNode): void { parentsWithUnmountedChildren = null } } + +/** + * dev / test only + */ +export function normalizeBlock(block: Block): Node[] { + if (!__DEV__ && !__TEST__) { + throw new Error( + 'normalizeBlock should not be used in production code paths', + ) + } + const nodes: Node[] = [] + if (block instanceof Node) { + nodes.push(block) + } else if (isArray(block)) { + block.forEach(child => nodes.push(...normalizeBlock(child))) + } else if (isVaporComponent(block)) { + nodes.push(...normalizeBlock(block.block!)) + } else { + nodes.push(...normalizeBlock(block.nodes)) + block.anchor && nodes.push(block.anchor) + } + return nodes +} diff --git a/packages/runtime-vapor/src/componentSlots.ts b/packages/runtime-vapor/src/componentSlots.ts index 33d5ff2334..cc6a825222 100644 --- a/packages/runtime-vapor/src/componentSlots.ts +++ b/packages/runtime-vapor/src/componentSlots.ts @@ -57,7 +57,10 @@ export const dynamicSlotsProxyHandlers: ProxyHandler = { deleteProperty: NO, } -export function getSlot(target: RawSlots, key: string): Slot | undefined { +export function getSlot( + target: RawSlots, + key: string, +): (Slot & { _bound?: Slot }) | undefined { if (key === '$') return const dynamicSources = target.$ if (dynamicSources) { @@ -116,10 +119,21 @@ export function createSlot( ? new Proxy(rawProps, dynamicSlotsPropsProxyHandlers) : EMPTY_OBJ - const renderSlot = (name: string) => { - const slot = getSlot(rawSlots, name) + const renderSlot = () => { + const slot = getSlot(rawSlots, isFunction(name) ? name() : name) if (slot) { - fragment.update(() => slot(slotProps) || (fallback && fallback())) + // create and cache bound version of the slot to make it stable + // so that we avoid unnecessary updates if it resolves to the same slot + fragment.update( + slot._bound || + (slot._bound = () => { + const slotContent = slot(slotProps) + if (slotContent instanceof DynamicFragment) { + slotContent.fallback = fallback + } + return slotContent + }), + ) } else { fragment.update(fallback) } @@ -127,9 +141,9 @@ export function createSlot( // dynamic slot name or has dynamicSlots if (isDynamicName || rawSlots.$) { - renderEffect(() => renderSlot(isFunction(name) ? name() : name)) + renderEffect(renderSlot) } else { - renderSlot(name) + renderSlot() } return fragment