Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(Transition): handle KeepAlive child unmount in Transition out-in mode #11833

Merged
merged 11 commits into from
Sep 6, 2024
3 changes: 2 additions & 1 deletion packages/runtime-core/src/componentRenderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -253,7 +254,7 @@ export function renderComponentRoot(
`that cannot be animated.`,
)
}
root.transition = vnode.transition
setTransitionHooks(root, vnode.transition)
}

if (__DEV__ && setRoot) {
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-core/src/components/BaseTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix #10827 and will not cause #11829

setTransitionHooks(vnode.component.subTree, hooks)
} else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/components/KeepAlive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ const KeepAliveImpl: ComponentOptions = {
pendingCacheKey = null

if (!slots.default) {
return null
return (current = null)
}

const children = slots.default()
Expand Down
171 changes: 170 additions & 1 deletion packages/vue/__tests__/e2e/Transition.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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', () => {
Expand Down