]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(runtime-vapor): switch to fallback when slot is empty
author三咲智子 Kevin Deng <sxzz@sxzz.moe>
Thu, 14 Nov 2024 19:47:35 +0000 (03:47 +0800)
committer三咲智子 Kevin Deng <sxzz@sxzz.moe>
Thu, 14 Nov 2024 19:57:46 +0000 (03:57 +0800)
packages/runtime-vapor/__tests__/componentSlots.spec.ts
packages/runtime-vapor/src/apiCreateIf.ts
packages/runtime-vapor/src/componentSlots.ts
playground/src/main.ts

index 987130820dd4d21b121fa85f944b5e27f9a6eacf..224e160e1806542453807cd6b978f3bccd8c9e3a 100644 (file)
@@ -365,7 +365,7 @@ describe('component: slots', () => {
   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
       })
@@ -589,7 +589,7 @@ describe('component: slots', () => {
         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 () => {
@@ -638,7 +638,7 @@ describe('component: slots', () => {
       const slotOutletName = ref('one')
 
       const Child = defineComponent(() => {
-        const temp0 = template('<p></p>')
+        const temp0 = template('<p>')
         const el0 = temp0()
         const slot1 = createSlot(
           () => slotOutletName.value,
@@ -672,5 +672,20 @@ describe('component: slots', () => {
 
       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>')
+    })
   })
 })
index b3e5799e286fc2d72a7c800ac83a052ed0267a35..4356fbf292469368f8a21af55199a54cfb587129 100644 (file)
@@ -1,6 +1,6 @@
 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
@@ -16,15 +16,14 @@ export const createIf = (
   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) {
@@ -47,7 +46,7 @@ export const createIf = (
 
   function doIf() {
     if ((newValue = !!condition()) !== oldValue) {
-      parent ||= anchor.parentNode
+      const parent = anchor.parentNode
       if (block) {
         scope!.stop()
         remove(block, parent!)
index 87d57ec8e13d998d39e534077a5f7828a7db3e09..0d321526f9b649fd0ad16268f83547725b921fbf 100644 (file)
@@ -4,6 +4,7 @@ import {
   effectScope,
   isReactive,
   shallowReactive,
+  shallowRef,
 } from '@vue/reactivity'
 import {
   type ComponentInternalInstance,
@@ -12,7 +13,13 @@ import {
 } 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'
@@ -107,27 +114,30 @@ export function initSlots(
 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: [],
@@ -137,29 +147,76 @@ export function createSlot(
 
   // 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 = []
+    }
   }
 }
 
@@ -214,3 +271,9 @@ function normalizeSlotProps(rawPropsList: NormalizedRawProps) {
     }
   }
 }
+
+function isValidBlock(block: Block) {
+  return (
+    normalizeBlock(block).filter(node => !(node instanceof Comment)).length > 0
+  )
+}
index c65d9c2ec395b1502999f7a473ecc08f10902ded..d2999613d4234e246013279148f652b3251f2d0d 100644 (file)
@@ -4,7 +4,7 @@ import { createVaporApp } from 'vue/vapor'
 import { createApp } from 'vue'
 import './style.css'
 
-const modules = import.meta.glob<any>('./**/*.(vue|js)')
+const modules = import.meta.glob<any>('./**/*.(vue|js|ts)')
 const mod = (modules['.' + location.pathname] || modules['./App.vue'])()
 
 mod.then(({ default: mod }) => {