VueTransition, KeepAlive and Suspense in the documented order, with async views
<script setup>
import { ref, shallowRef, defineComponent, h, onMounted, onActivated } from "vue";
const makeView = (name, ms) =>
defineComponent({
name,
async setup() {
onMounted(() => console.log(` ${name}: mounted`));
onActivated(() => console.log(` ${name}: activated`));
const clicks = ref(0);
console.log(` ${name}: setup started`);
await new Promise((r) => setTimeout(r, ms));
return () => h("button", { class: "view", onClick: () => clicks.value++ }, `${name} clicks ${clicks.value}`);
},
});
const views = { Home: makeView("Home", 50), Reports: makeView("Reports", 80) };
const current = shallowRef(views.Home);
</script>
<template>
<nav>
<button v-for="(v, name) in views" :key="name" class="nav" @click="current = v">{{ name }}</button>
</nav>
<Transition mode="out-in" name="fade" :duration="40">
<KeepAlive>
<Suspense @pending="console.log(' suspense: pending')" @fallback="console.log(' suspense: fallback shown')" @resolve="console.log(' suspense: resolved')">
<component :is="current" />
<template #fallback>
<p class="loading">Loading...</p>
</template>
</Suspense>
</KeepAlive>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.04s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
The same stack around RouterView with vue-router 5, run in jsdom
import { JSDOM } from "jsdom";
const dom = new JSDOM('<div id="app"></div>', { pretendToBeVisual: true, url: "http://localhost/" });
for (const k of ["window", "document", "Node", "Element", "HTMLElement", "SVGElement", "Text", "Comment", "requestAnimationFrame", "getComputedStyle", "history", "location"]) globalThis[k] = dom.window[k];
const { createApp, defineComponent, h, ref } = await import("vue");
const { createRouter, createMemoryHistory } = await import("vue-router");
const page = (name, ms) => defineComponent({
name,
async setup() {
const n = ref(0);
await new Promise((r) => setTimeout(r, ms));
return () => h("button", { class: "page", onClick: () => n.value++ }, `${name} ${n.value}`);
},
});
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: "/", component: page("Home", 30) }, { path: "/reports", component: page("Reports", 30) }],
});
const App = {
template: `
<RouterView v-slot="{ Component }">
<template v-if="Component">
<Transition mode="out-in" :duration="20">
<KeepAlive>
<Suspense>
<component :is="Component" />
<template #fallback><p class="page">Loading...</p></template>
</Suspense>
</KeepAlive>
</Transition>
</template>
</RouterView>`,
};
const app = createApp(App).use(router);
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const screen = () => document.querySelector(".page")?.textContent;
await router.push("/"); app.mount("#app");
console.log("first paint :", screen());
await wait(200); console.log("+200ms :", screen());
document.querySelector(".page").click(); document.querySelector(".page").click(); await wait(20); console.log("2 clicks :", screen());
await router.push("/reports"); await wait(200); console.log("/reports :", screen());
await router.push("/"); await wait(200); console.log("back to / :", screen());