]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(Transition): handle KeepAlive child unmount in Transition out-in mode (#11833)
authoredison <daiwei521@126.com>
Fri, 6 Sep 2024 03:03:58 +0000 (11:03 +0800)
committerGitHub <noreply@github.com>
Fri, 6 Sep 2024 03:03:58 +0000 (11:03 +0800)
close #11775

packages/runtime-core/src/componentRenderUtils.ts
packages/runtime-core/src/components/BaseTransition.ts
packages/runtime-core/src/components/KeepAlive.ts
packages/vue/__tests__/e2e/Transition.spec.ts

index 5badb04b006a815c31801d6c7060a29cf837b49b..a15d18d56bf70b7b74a96926174446afae4ab4c9 100644 (file)
@@ -27,6 +27,7 @@ import {
   warnDeprecation,
 } from './compat/compatConfig'
 import { shallowReadonly } from '@vue/reactivity'
+import { setTransitionHooks } from './components/BaseTransition'
 
 /**
  * dev only flag to track whether $attrs was used during render.
@@ -253,7 +254,7 @@ export function renderComponentRoot(
           `that cannot be animated.`,
       )
     }
-    root.transition = vnode.transition
+    setTransitionHooks(root, vnode.transition)
   }
 
   if (__DEV__ && setRoot) {
index 37534ad699fc654c94571fb3aea8524522dd3e43..6ce06d2823939febacf463ed0e3e6ad5f79213bc 100644 (file)
@@ -227,6 +227,7 @@ const BaseTransitionImpl: ComponentOptions = {
             if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
               instance.update()
             }
+            delete leavingHooks.afterLeave
           }
           return emptyPlaceholder(child)
         } else if (mode === 'in-out' && innerChild.type !== Comment) {
@@ -515,6 +516,7 @@ function getInnerChild(vnode: VNode): VNode | undefined {
 
 export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void {
   if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
+    vnode.transition = hooks
     setTransitionHooks(vnode.component.subTree, hooks)
   } else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
     vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
index a87f44cc8fade0c0b4250b3f43a303a14df26343..dd1d1f5a6e3addf044c2c25e9582a348827af35e 100644 (file)
@@ -267,7 +267,7 @@ const KeepAliveImpl: ComponentOptions = {
       pendingCacheKey = null
 
       if (!slots.default) {
-        return null
+        return (current = null)
       }
 
       const children = slots.default()
index 9a5375e72a255993c9f84c9c853f938b9c483565..c0863a75991be303db3781088c49a0a708dfd019 100644 (file)
@@ -1427,9 +1427,11 @@ describe('e2e: Transition', () => {
       },
       E2E_TIMEOUT,
     )
+  })
 
+  describe('transition with KeepAlive', () => {
     test(
-      'w/ KeepAlive + unmount innerChild',
+      'unmount innerChild (out-in mode)',
       async () => {
         const unmountSpy = vi.fn()
         await page().exposeFunction('unmountSpy', unmountSpy)
@@ -1484,6 +1486,173 @@ describe('e2e: Transition', () => {
       },
       E2E_TIMEOUT,
     )
+
+    // #11775
+    test(
+      'switch child then update include (out-in mode)',
+      async () => {
+        const onUpdatedSpyA = vi.fn()
+        const onUnmountedSpyC = vi.fn()
+
+        await page().exposeFunction('onUpdatedSpyA', onUpdatedSpyA)
+        await page().exposeFunction('onUnmountedSpyC', onUnmountedSpyC)
+
+        await page().evaluate(() => {
+          const { onUpdatedSpyA, onUnmountedSpyC } = window as any
+          const { createApp, ref, shallowRef, h, onUpdated, onUnmounted } = (
+            window as any
+          ).Vue
+          createApp({
+            template: `
+            <div id="container">
+              <transition mode="out-in">
+                <KeepAlive :include="includeRef">
+                  <component :is="current" />
+                </KeepAlive>
+              </transition>
+            </div>
+            <button id="switchToB" @click="switchToB">switchToB</button>
+            <button id="switchToC" @click="switchToC">switchToC</button>
+            <button id="switchToA" @click="switchToA">switchToA</button>
+          `,
+            components: {
+              CompA: {
+                name: 'CompA',
+                setup() {
+                  onUpdated(onUpdatedSpyA)
+                  return () => h('div', 'CompA')
+                },
+              },
+              CompB: {
+                name: 'CompB',
+                setup() {
+                  return () => h('div', 'CompB')
+                },
+              },
+              CompC: {
+                name: 'CompC',
+                setup() {
+                  onUnmounted(onUnmountedSpyC)
+                  return () => h('div', 'CompC')
+                },
+              },
+            },
+            setup: () => {
+              const includeRef = ref(['CompA', 'CompB', 'CompC'])
+              const current = shallowRef('CompA')
+              const switchToB = () => (current.value = 'CompB')
+              const switchToC = () => (current.value = 'CompC')
+              const switchToA = () => {
+                current.value = 'CompA'
+                includeRef.value = ['CompA']
+              }
+              return { current, switchToB, switchToC, switchToA, includeRef }
+            },
+          }).mount('#app')
+        })
+
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div>CompA</div>')
+
+        await click('#switchToB')
+        await nextTick()
+        await click('#switchToC')
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div class="">CompC</div>')
+
+        await click('#switchToA')
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div class="">CompA</div>')
+
+        // expect CompA only update once
+        expect(onUpdatedSpyA).toBeCalledTimes(1)
+        expect(onUnmountedSpyC).toBeCalledTimes(1)
+      },
+      E2E_TIMEOUT,
+    )
+
+    // #10827
+    test(
+      'switch and update child then update include (out-in mode)',
+      async () => {
+        const onUnmountedSpyB = vi.fn()
+        await page().exposeFunction('onUnmountedSpyB', onUnmountedSpyB)
+
+        await page().evaluate(() => {
+          const { onUnmountedSpyB } = window as any
+          const {
+            createApp,
+            ref,
+            shallowRef,
+            h,
+            provide,
+            inject,
+            onUnmounted,
+          } = (window as any).Vue
+          createApp({
+            template: `
+            <div id="container">
+              <transition name="test-anim" mode="out-in">
+                <KeepAlive :include="includeRef">
+                  <component :is="current" />
+                </KeepAlive>
+              </transition>
+            </div>
+            <button id="switchToA" @click="switchToA">switchToA</button>
+            <button id="switchToB" @click="switchToB">switchToB</button>
+          `,
+            components: {
+              CompA: {
+                name: 'CompA',
+                setup() {
+                  const current = inject('current')
+                  return () => h('div', current.value)
+                },
+              },
+              CompB: {
+                name: 'CompB',
+                setup() {
+                  const current = inject('current')
+                  onUnmounted(onUnmountedSpyB)
+                  return () => h('div', current.value)
+                },
+              },
+            },
+            setup: () => {
+              const includeRef = ref(['CompA'])
+              const current = shallowRef('CompA')
+              provide('current', current)
+
+              const switchToB = () => {
+                current.value = 'CompB'
+                includeRef.value = ['CompA', 'CompB']
+              }
+              const switchToA = () => {
+                current.value = 'CompA'
+                includeRef.value = ['CompA']
+              }
+              return { current, switchToB, switchToA, includeRef }
+            },
+          }).mount('#app')
+        })
+
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div>CompA</div>')
+
+        await click('#switchToB')
+        await transitionFinish()
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div class="">CompB</div>')
+
+        await click('#switchToA')
+        await transitionFinish()
+        await transitionFinish()
+        expect(await html('#container')).toBe('<div class="">CompA</div>')
+
+        expect(onUnmountedSpyB).toBeCalledTimes(1)
+      },
+      E2E_TIMEOUT,
+    )
   })
 
   describe('transition with Suspense', () => {