Skip to content

Commit 6b7901d

Browse files
authored
fix(Transition): handle KeepAlive child unmount in Transition out-in mode (#11833)
close #11775
1 parent 7e3b3bb commit 6b7901d

File tree

4 files changed

+175
-3
lines changed

4 files changed

+175
-3
lines changed

packages/runtime-core/src/componentRenderUtils.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
warnDeprecation,
2828
} from './compat/compatConfig'
2929
import { shallowReadonly } from '@vue/reactivity'
30+
import { setTransitionHooks } from './components/BaseTransition'
3031

3132
/**
3233
* dev only flag to track whether $attrs was used during render.
@@ -253,7 +254,7 @@ export function renderComponentRoot(
253254
`that cannot be animated.`,
254255
)
255256
}
256-
root.transition = vnode.transition
257+
setTransitionHooks(root, vnode.transition)
257258
}
258259

259260
if (__DEV__ && setRoot) {

packages/runtime-core/src/components/BaseTransition.ts

+2
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ const BaseTransitionImpl: ComponentOptions = {
227227
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
228228
instance.update()
229229
}
230+
delete leavingHooks.afterLeave
230231
}
231232
return emptyPlaceholder(child)
232233
} else if (mode === 'in-out' && innerChild.type !== Comment) {
@@ -515,6 +516,7 @@ function getInnerChild(vnode: VNode): VNode | undefined {
515516

516517
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks): void {
517518
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
519+
vnode.transition = hooks
518520
setTransitionHooks(vnode.component.subTree, hooks)
519521
} else if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
520522
vnode.ssContent!.transition = hooks.clone(vnode.ssContent!)

packages/runtime-core/src/components/KeepAlive.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ const KeepAliveImpl: ComponentOptions = {
267267
pendingCacheKey = null
268268

269269
if (!slots.default) {
270-
return null
270+
return (current = null)
271271
}
272272

273273
const children = slots.default()

packages/vue/__tests__/e2e/Transition.spec.ts

+170-1
Original file line numberDiff line numberDiff line change
@@ -1427,9 +1427,11 @@ describe('e2e: Transition', () => {
14271427
},
14281428
E2E_TIMEOUT,
14291429
)
1430+
})
14301431

1432+
describe('transition with KeepAlive', () => {
14311433
test(
1432-
'w/ KeepAlive + unmount innerChild',
1434+
'unmount innerChild (out-in mode)',
14331435
async () => {
14341436
const unmountSpy = vi.fn()
14351437
await page().exposeFunction('unmountSpy', unmountSpy)
@@ -1484,6 +1486,173 @@ describe('e2e: Transition', () => {
14841486
},
14851487
E2E_TIMEOUT,
14861488
)
1489+
1490+
// #11775
1491+
test(
1492+
'switch child then update include (out-in mode)',
1493+
async () => {
1494+
const onUpdatedSpyA = vi.fn()
1495+
const onUnmountedSpyC = vi.fn()
1496+
1497+
await page().exposeFunction('onUpdatedSpyA', onUpdatedSpyA)
1498+
await page().exposeFunction('onUnmountedSpyC', onUnmountedSpyC)
1499+
1500+
await page().evaluate(() => {
1501+
const { onUpdatedSpyA, onUnmountedSpyC } = window as any
1502+
const { createApp, ref, shallowRef, h, onUpdated, onUnmounted } = (
1503+
window as any
1504+
).Vue
1505+
createApp({
1506+
template: `
1507+
<div id="container">
1508+
<transition mode="out-in">
1509+
<KeepAlive :include="includeRef">
1510+
<component :is="current" />
1511+
</KeepAlive>
1512+
</transition>
1513+
</div>
1514+
<button id="switchToB" @click="switchToB">switchToB</button>
1515+
<button id="switchToC" @click="switchToC">switchToC</button>
1516+
<button id="switchToA" @click="switchToA">switchToA</button>
1517+
`,
1518+
components: {
1519+
CompA: {
1520+
name: 'CompA',
1521+
setup() {
1522+
onUpdated(onUpdatedSpyA)
1523+
return () => h('div', 'CompA')
1524+
},
1525+
},
1526+
CompB: {
1527+
name: 'CompB',
1528+
setup() {
1529+
return () => h('div', 'CompB')
1530+
},
1531+
},
1532+
CompC: {
1533+
name: 'CompC',
1534+
setup() {
1535+
onUnmounted(onUnmountedSpyC)
1536+
return () => h('div', 'CompC')
1537+
},
1538+
},
1539+
},
1540+
setup: () => {
1541+
const includeRef = ref(['CompA', 'CompB', 'CompC'])
1542+
const current = shallowRef('CompA')
1543+
const switchToB = () => (current.value = 'CompB')
1544+
const switchToC = () => (current.value = 'CompC')
1545+
const switchToA = () => {
1546+
current.value = 'CompA'
1547+
includeRef.value = ['CompA']
1548+
}
1549+
return { current, switchToB, switchToC, switchToA, includeRef }
1550+
},
1551+
}).mount('#app')
1552+
})
1553+
1554+
await transitionFinish()
1555+
expect(await html('#container')).toBe('<div>CompA</div>')
1556+
1557+
await click('#switchToB')
1558+
await nextTick()
1559+
await click('#switchToC')
1560+
await transitionFinish()
1561+
expect(await html('#container')).toBe('<div class="">CompC</div>')
1562+
1563+
await click('#switchToA')
1564+
await transitionFinish()
1565+
expect(await html('#container')).toBe('<div class="">CompA</div>')
1566+
1567+
// expect CompA only update once
1568+
expect(onUpdatedSpyA).toBeCalledTimes(1)
1569+
expect(onUnmountedSpyC).toBeCalledTimes(1)
1570+
},
1571+
E2E_TIMEOUT,
1572+
)
1573+
1574+
// #10827
1575+
test(
1576+
'switch and update child then update include (out-in mode)',
1577+
async () => {
1578+
const onUnmountedSpyB = vi.fn()
1579+
await page().exposeFunction('onUnmountedSpyB', onUnmountedSpyB)
1580+
1581+
await page().evaluate(() => {
1582+
const { onUnmountedSpyB } = window as any
1583+
const {
1584+
createApp,
1585+
ref,
1586+
shallowRef,
1587+
h,
1588+
provide,
1589+
inject,
1590+
onUnmounted,
1591+
} = (window as any).Vue
1592+
createApp({
1593+
template: `
1594+
<div id="container">
1595+
<transition name="test-anim" mode="out-in">
1596+
<KeepAlive :include="includeRef">
1597+
<component :is="current" />
1598+
</KeepAlive>
1599+
</transition>
1600+
</div>
1601+
<button id="switchToA" @click="switchToA">switchToA</button>
1602+
<button id="switchToB" @click="switchToB">switchToB</button>
1603+
`,
1604+
components: {
1605+
CompA: {
1606+
name: 'CompA',
1607+
setup() {
1608+
const current = inject('current')
1609+
return () => h('div', current.value)
1610+
},
1611+
},
1612+
CompB: {
1613+
name: 'CompB',
1614+
setup() {
1615+
const current = inject('current')
1616+
onUnmounted(onUnmountedSpyB)
1617+
return () => h('div', current.value)
1618+
},
1619+
},
1620+
},
1621+
setup: () => {
1622+
const includeRef = ref(['CompA'])
1623+
const current = shallowRef('CompA')
1624+
provide('current', current)
1625+
1626+
const switchToB = () => {
1627+
current.value = 'CompB'
1628+
includeRef.value = ['CompA', 'CompB']
1629+
}
1630+
const switchToA = () => {
1631+
current.value = 'CompA'
1632+
includeRef.value = ['CompA']
1633+
}
1634+
return { current, switchToB, switchToA, includeRef }
1635+
},
1636+
}).mount('#app')
1637+
})
1638+
1639+
await transitionFinish()
1640+
expect(await html('#container')).toBe('<div>CompA</div>')
1641+
1642+
await click('#switchToB')
1643+
await transitionFinish()
1644+
await transitionFinish()
1645+
expect(await html('#container')).toBe('<div class="">CompB</div>')
1646+
1647+
await click('#switchToA')
1648+
await transitionFinish()
1649+
await transitionFinish()
1650+
expect(await html('#container')).toBe('<div class="">CompA</div>')
1651+
1652+
expect(onUnmountedSpyB).toBeCalledTimes(1)
1653+
},
1654+
E2E_TIMEOUT,
1655+
)
14871656
})
14881657

14891658
describe('transition with Suspense', () => {

0 commit comments

Comments
 (0)