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 #11778

Merged
merged 10 commits into from
Sep 5, 2024
Merged
5 changes: 4 additions & 1 deletion packages/runtime-core/src/componentRenderUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export function renderComponentRoot(
setupState,
ctx,
inheritAttrs,
isMounted,
} = instance
const prev = setCurrentRenderingInstance(instance)

Expand Down Expand Up @@ -253,7 +254,9 @@ export function renderComponentRoot(
`that cannot be animated.`,
)
}
root.transition = vnode.transition
root.transition = isMounted
Copy link
Member Author

Choose a reason for hiding this comment

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

this line fixes #10827

? vnode.component!.subTree.transition!
: vnode.transition
}

if (__DEV__ && setRoot) {
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-core/src/components/BaseTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ const BaseTransitionImpl: ComponentOptions = {
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
instance.update()
}
delete leavingHooks.afterLeave
Copy link
Member Author

Choose a reason for hiding this comment

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

this line fixes problem 1

}
return emptyPlaceholder(child)
} else if (mode === 'in-out' && innerChild.type !== Comment) {
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)
Copy link
Member Author

Choose a reason for hiding this comment

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

this line fixes problem 2, but it will lead to #10827 regression.

}

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