diff --git a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts index 829421ca2..754d83ac6 100644 --- a/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts +++ b/packages/runtime-vapor/__tests__/components/KeepAlive.spec.ts @@ -5,7 +5,9 @@ import { onDeactivated, onMounted, onUnmounted, + reactive, ref, + shallowRef, } from 'vue' import type { LooseRawProps, VaporComponent } from '../../src/component' import { makeRender } from '../_utils' @@ -1037,4 +1039,151 @@ describe('VaporKeepAlive', () => { expect(html()).toBe(`
1
`) }) }) + + test.todo('should work with async component', async () => {}) + + test('handle error in async onActivated', async () => { + const err = new Error('foo') + const handler = vi.fn() + const Child = defineVaporComponent({ + setup() { + onActivated(async () => { + throw err + }) + + return template(` createComponent(Child), + }) + }, + }).create() + + app.config.errorHandler = handler + app.mount(document.createElement('div')) + + await nextTick() + expect(handler).toHaveBeenCalledTimes(1) + }) + + test('should avoid unmount later included components', async () => { + const unmountedA = vi.fn() + const mountedA = vi.fn() + const activatedA = vi.fn() + const deactivatedA = vi.fn() + const unmountedB = vi.fn() + const mountedB = vi.fn() + + const A = defineVaporComponent({ + name: 'A', + setup() { + onMounted(mountedA) + onUnmounted(unmountedA) + onActivated(activatedA) + onDeactivated(deactivatedA) + return template(`
A
`)() + }, + }) + + const B = defineVaporComponent({ + name: 'B', + setup() { + onMounted(mountedB) + onUnmounted(unmountedB) + return template(`
B
`)() + }, + }) + + const include = reactive([]) + const current = shallowRef(A) + const { html } = define({ + setup() { + return createComponent( + VaporKeepAlive, + { include: () => include }, + { + default: () => createDynamicComponent(() => current.value), + }, + ) + }, + }).render() + + expect(html()).toBe(`
A
`) + expect(mountedA).toHaveBeenCalledTimes(1) + expect(unmountedA).toHaveBeenCalledTimes(0) + expect(activatedA).toHaveBeenCalledTimes(0) + expect(deactivatedA).toHaveBeenCalledTimes(0) + expect(mountedB).toHaveBeenCalledTimes(0) + expect(unmountedB).toHaveBeenCalledTimes(0) + + include.push('A') // cache A + await nextTick() + current.value = B // toggle to B + await nextTick() + expect(html()).toBe(`
B
`) + expect(mountedA).toHaveBeenCalledTimes(1) + expect(unmountedA).toHaveBeenCalledTimes(0) + expect(activatedA).toHaveBeenCalledTimes(0) + expect(deactivatedA).toHaveBeenCalledTimes(1) + expect(mountedB).toHaveBeenCalledTimes(1) + expect(unmountedB).toHaveBeenCalledTimes(0) + }) + + test('remove component from include then switching child', async () => { + const About = defineVaporComponent({ + name: 'About', + setup() { + return template(`

About

`)() + }, + }) + const mountedHome = vi.fn() + const unmountedHome = vi.fn() + const activatedHome = vi.fn() + const deactivatedHome = vi.fn() + + const Home = defineVaporComponent({ + name: 'Home', + setup() { + onMounted(mountedHome) + onUnmounted(unmountedHome) + onDeactivated(deactivatedHome) + onActivated(activatedHome) + return template(`

Home

`)() + }, + }) + + const activeViewName = ref('Home') + const cacheList = reactive(['Home']) + + define({ + setup() { + return createComponent( + VaporKeepAlive, + { include: () => cacheList }, + { + default: () => { + return createIf( + () => activeViewName.value === 'Home', + () => createComponent(Home), + () => createComponent(About), + ) + }, + }, + ) + }, + }).render() + + expect(mountedHome).toHaveBeenCalledTimes(1) + expect(activatedHome).toHaveBeenCalledTimes(1) + cacheList.splice(0, 1) + await nextTick() + activeViewName.value = 'About' + await nextTick() + expect(deactivatedHome).toHaveBeenCalledTimes(0) + expect(unmountedHome).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-vapor/src/components/KeepAlive.ts b/packages/runtime-vapor/src/components/KeepAlive.ts index ec7cce0be..127f0c94f 100644 --- a/packages/runtime-vapor/src/components/KeepAlive.ts +++ b/packages/runtime-vapor/src/components/KeepAlive.ts @@ -12,7 +12,13 @@ import { warn, watch, } from '@vue/runtime-dom' -import { type Block, insert, isFragment, isValidBlock } from '../block' +import { + type Block, + DynamicFragment, + insert, + isFragment, + isValidBlock, +} from '../block' import { type ObjectVaporComponent, type VaporComponent, @@ -153,7 +159,7 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ } } - const children = slots.default() + let children = slots.default() if (isArray(children) && children.length > 1) { if (__DEV__) { warn(`KeepAlive should contain exactly one component child.`) @@ -161,6 +167,13 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ return children } + // wrap children in dynamic fragment + if (!isFragment(children)) { + const frag = new DynamicFragment() + frag.update(() => children) + children = frag + } + function pruneCache(filter: (name: string) => boolean) { cache.forEach((instance, key) => { const name = getComponentName(instance.type) @@ -197,16 +210,6 @@ export const VaporKeepAliveImpl: ObjectVaporComponent = defineVaporComponent({ }, }) -export const VaporKeepAlive = VaporKeepAliveImpl as any as { - __isKeepAlive: true - new (): { - $props: KeepAliveProps - $slots: { - default(): Block - } - } -} - function getInnerBlock(block: Block): VaporComponentInstance | undefined { if (isVaporComponent(block)) { return block diff --git a/packages/runtime-vapor/src/index.ts b/packages/runtime-vapor/src/index.ts index 6cc06a72e..f656520e3 100644 --- a/packages/runtime-vapor/src/index.ts +++ b/packages/runtime-vapor/src/index.ts @@ -3,7 +3,7 @@ export { createVaporApp, createVaporSSRApp } from './apiCreateApp' export { defineVaporComponent } from './apiDefineComponent' export { vaporInteropPlugin } from './vdomInterop' export type { VaporDirective } from './directives/custom' -export { VaporKeepAlive } from './components/KeepAlive' +export { VaporKeepAliveImpl as VaporKeepAlive } from './components/KeepAlive' // compiler-use only export { insert, prepend, remove, isFragment, VaporFragment } from './block'