diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 5badb04b006..a15d18d56bf 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -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) { diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index 37534ad699f..6ce06d28239 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -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!) diff --git a/packages/runtime-core/src/components/KeepAlive.ts b/packages/runtime-core/src/components/KeepAlive.ts index a87f44cc8fa..dd1d1f5a6e3 100644 --- a/packages/runtime-core/src/components/KeepAlive.ts +++ b/packages/runtime-core/src/components/KeepAlive.ts @@ -267,7 +267,7 @@ const KeepAliveImpl: ComponentOptions = { pendingCacheKey = null if (!slots.default) { - return null + return (current = null) } const children = slots.default() diff --git a/packages/vue/__tests__/e2e/Transition.spec.ts b/packages/vue/__tests__/e2e/Transition.spec.ts index 9a5375e72a2..c0863a75991 100644 --- a/packages/vue/__tests__/e2e/Transition.spec.ts +++ b/packages/vue/__tests__/e2e/Transition.spec.ts @@ -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: ` +
+ + + + + +
+ + + + `, + 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('
CompA
') + + await click('#switchToB') + await nextTick() + await click('#switchToC') + await transitionFinish() + expect(await html('#container')).toBe('
CompC
') + + await click('#switchToA') + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + // 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: ` +
+ + + + + +
+ + + `, + 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('
CompA
') + + await click('#switchToB') + await transitionFinish() + await transitionFinish() + expect(await html('#container')).toBe('
CompB
') + + await click('#switchToA') + await transitionFinish() + await transitionFinish() + expect(await html('#container')).toBe('
CompA
') + + expect(onUnmountedSpyB).toBeCalledTimes(1) + }, + E2E_TIMEOUT, + ) }) describe('transition with Suspense', () => {