]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip(vapor): handle slot fallback when content changes
authorEvan You <evan@vuejs.org>
Sat, 14 Dec 2024 14:15:34 +0000 (22:15 +0800)
committerEvan You <evan@vuejs.org>
Sat, 14 Dec 2024 14:17:16 +0000 (22:17 +0800)
packages/runtime-vapor/__tests__/componentSlots.spec.ts
packages/runtime-vapor/src/block.ts
packages/runtime-vapor/src/componentSlots.ts

index 868e45671c08f22ce71068e01da2a59118134955..f4ff0c31cba2ff7a2039260552d8331ba75865ba 100644 (file)
@@ -3,6 +3,7 @@
 import {
   createComponent,
   createForSlots,
+  createIf,
   createSlot,
   createVaporApp,
   defineVaporComponent,
@@ -430,5 +431,42 @@ describe('component: slots', () => {
 
       expect(host.innerHTML).toBe('<p><!--slot--></p>')
     })
+
+    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<!--if--><!--slot-->')
+
+      toggle.value = false
+      await nextTick()
+      expect(html()).toBe('fallback<!--if--><!--slot-->')
+
+      toggle.value = true
+      await nextTick()
+      expect(html()).toBe('content<!--if--><!--slot-->')
+    })
   })
 })
index eccd573322bad271da4e785b9003ffa8c7290c2a..d1daa8660daca75bdd68f0e9bcccf359d55c3194 100644 (file)
@@ -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<unknown>): 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
+}
index 33d5ff23345506cbd98513437508a95db5980095..cc6a825222e1b7acf73aed175b5e8f3723170d9e 100644 (file)
@@ -57,7 +57,10 @@ export const dynamicSlotsProxyHandlers: ProxyHandler<RawSlots> = {
   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