From b8adab47e0f87fe6a0db3ac2d4f3a4548a643186 Mon Sep 17 00:00:00 2001 From: Peter Hurst Date: Sat, 22 Aug 2026 00:48:43 +0100 Subject: [PATCH] =?UTF-8?q?feat(docs):=20678=20=E2=80=94=20routing=20demo?= =?UTF-8?q?=20that=20switches=20on=20a=20page=20route=20atom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A deliberately minimal demo: one route atom holds which of a fixed set of pages the URL names, a plain `switch` on its value picks the component, and the nav buttons navigate by writing that atom. It imports nothing from `jarl-react` - no `Link`, `Route` or `Switch` component anywhere in it. The fixed set is composed from existing exports rather than a new primitive: a `paramRouteAtom` narrowed by a `transformRouteAtom`, the same composition `numericRouteAtom` documents, which also types the value as the union the switch needs so its cases are exhaustive. Ticket: 678 --- packages/docs/src/App.tsx | 5 + packages/docs/src/demos/SwitchRoutingApp.tsx | 113 ++++++++++++++++++ packages/docs/src/demos/switchRoutingPages.ts | 19 +++ packages/docs/src/pages/DemosIndex.tsx | 8 ++ packages/docs/src/pages/SwitchRoutingDemo.tsx | 15 +++ packages/docs/src/router/routes.ts | 14 ++- 6 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 packages/docs/src/demos/SwitchRoutingApp.tsx create mode 100644 packages/docs/src/demos/switchRoutingPages.ts create mode 100644 packages/docs/src/pages/SwitchRoutingDemo.tsx diff --git a/packages/docs/src/App.tsx b/packages/docs/src/App.tsx index 4217f81..1ec3920 100644 --- a/packages/docs/src/App.tsx +++ b/packages/docs/src/App.tsx @@ -11,6 +11,7 @@ import { historyRoute, demosIndexRoute, basicRoutingDemoRoute, + switchRoutingDemoRoute, blogRoutingDemoRoute, dataGridDemoRoute, complexRoutingDemoRoute, @@ -23,6 +24,7 @@ import Changelog from "./pages/Changelog"; import History from "./pages/History"; import DemosIndex from "./pages/DemosIndex"; import BasicRoutingDemo from "./pages/BasicRoutingDemo"; +import SwitchRoutingDemo from "./pages/SwitchRoutingDemo"; import BlogRoutingDemo from "./pages/BlogRoutingDemo"; import DataGridDemo from "./pages/DataGridDemo"; import ComplexRoutingDemo from "./pages/ComplexRoutingDemo"; @@ -63,6 +65,9 @@ export const App = () => ( + + + diff --git a/packages/docs/src/demos/SwitchRoutingApp.tsx b/packages/docs/src/demos/SwitchRoutingApp.tsx new file mode 100644 index 0000000..b084142 --- /dev/null +++ b/packages/docs/src/demos/SwitchRoutingApp.tsx @@ -0,0 +1,113 @@ +import { atom, useAtomValue, useSetAtom } from "jotai"; +import { createRootAtom, paramRouteAtom, transformRouteAtom } from "jarl-atoms"; +import { + SwitchRoutingPage, + isSwitchRoutingPage, + switchRoutingPageLabels, + switchRoutingPages, +} from "./switchRoutingPages"; + +// The page this demo is mounted on, so everything below it is a plain module-level atom. +const switchRoutingRoot = createRootAtom({ basePath: "/demos/switch-routing" }); + +// One segment out of a fixed set: a paramRouteAtom narrowed by a transformRouteAtom, which also +// types the value as the union the switch below needs. +const pageRoute = transformRouteAtom<{ page: string }, { page: SwitchRoutingPage }>( + paramRouteAtom("page", { parent: switchRoutingRoot }), + ({ page }) => (isSwitchRoutingPage(page) ? { page } : undefined), + ({ page }) => ({ page }), +); + +const currentPageAtom = atom((get): SwitchRoutingPage | "not-found" => { + const page = get(pageRoute); + if (page.match) return page.values.page; + return get(switchRoutingRoot).exact ? "home" : "not-found"; +}); + +const HomePage = () => ( +
+

Home

+

+ Nothing here is a <Route>. One route atom holds which page the URL names, and a plain{" "} + switch on that value picks the component to render. +

+
+); + +const AboutPage = () => ( +
+

About

+

+ The buttons above navigated here by writing to that same atom — {`navigate({ page: "about" })`}{" "} + — rather than by following a <Link>. Writing it set the URL; reading it back is what + moved the switch. +

+
+); + +const ContactPage = () => ( +
+

Contact

+

+ Back and forward need nothing extra: the atom derives its value from the location, so the browser's history + moves the switch exactly the way a button does. +

+
+); + +const NotFoundPage = () => ( +
+

No such page

+

+ The page route matches only the segments named above, so any other URL under this demo leaves it unmatched and the + switch falls through to this case. +

+
+); + +const pageView = (page: SwitchRoutingPage | "not-found") => { + switch (page) { + case "home": + return ; + case "about": + return ; + case "contact": + return ; + case "not-found": + return ; + } +}; + +const SwitchRoutingNav = ({ + current, + onNavigate, +}: { + current: SwitchRoutingPage | "not-found"; + onNavigate: (page: SwitchRoutingPage) => void; +}) => ( + +); + +/** + * Self-contained demo of the least routing machinery that still routes: a `switch` on one route + * atom's value picks the page, and navigation writes to that atom directly. + */ +export const SwitchRoutingApp = () => { + const page = useAtomValue(currentPageAtom); + const navigate = useSetAtom(pageRoute); + + return ( + <> + navigate({ page: next })} /> + {pageView(page)} + + ); +}; + +export default SwitchRoutingApp; diff --git a/packages/docs/src/demos/switchRoutingPages.ts b/packages/docs/src/demos/switchRoutingPages.ts new file mode 100644 index 0000000..9dfe029 --- /dev/null +++ b/packages/docs/src/demos/switchRoutingPages.ts @@ -0,0 +1,19 @@ +/** Every page this demo has: the switch's cases, and the only segments its page route matches. */ +export const switchRoutingPages = ["home", "about", "contact"] as const; + +export type SwitchRoutingPage = (typeof switchRoutingPages)[number]; + +export const isSwitchRoutingPage = (segment: string): segment is SwitchRoutingPage => + (switchRoutingPages as readonly string[]).includes(segment); + +export const switchRoutingPageLabels: Record = { + home: "Home", + about: "About", + contact: "Contact", +}; + +/** Every concrete path this demo's SSG build should prerender. */ +export const switchRoutingStaticPaths = (): string[] => [ + "/demos/switch-routing", + ...switchRoutingPages.map((page) => `/demos/switch-routing/${page}`), +]; diff --git a/packages/docs/src/pages/DemosIndex.tsx b/packages/docs/src/pages/DemosIndex.tsx index 7d52440..1e0cecb 100644 --- a/packages/docs/src/pages/DemosIndex.tsx +++ b/packages/docs/src/pages/DemosIndex.tsx @@ -6,6 +6,7 @@ import { blogRoutingDemoRoute, complexRoutingDemoRoute, dataGridDemoRoute, + switchRoutingDemoRoute, } from "../router/routes"; export const DemosIndex = () => ( @@ -26,6 +27,13 @@ export const DemosIndex = () => ( — a nested router-within-a-router built from staticRouteAtom/paramRouteAtom and the atoms-based Link/Route components. +
  • + + Switch-statement routing + {" "} + — the same job with no routing components at all: a switch on one route atom's value + picks the page, and the nav buttons navigate by writing to that atom instead of rendering a Link. +
  • Blog routing diff --git a/packages/docs/src/pages/SwitchRoutingDemo.tsx b/packages/docs/src/pages/SwitchRoutingDemo.tsx new file mode 100644 index 0000000..4a1022d --- /dev/null +++ b/packages/docs/src/pages/SwitchRoutingDemo.tsx @@ -0,0 +1,15 @@ +import { SwitchRoutingApp } from "../demos/SwitchRoutingApp"; +import DemoPage from "../lib/DemoPage"; +import demoSource from "../demos/SwitchRoutingApp.tsx?raw"; + +export const SwitchRoutingDemo = () => ( + + + +); + +export default SwitchRoutingDemo; diff --git a/packages/docs/src/router/routes.ts b/packages/docs/src/router/routes.ts index 8b0c945..d8d1df4 100644 --- a/packages/docs/src/router/routes.ts +++ b/packages/docs/src/router/routes.ts @@ -9,6 +9,7 @@ import { atom } from "jotai"; import { asyncRouteAtom, notAtom, rootAtom, staticRouteAtom, paramRouteAtom } from "jarl-atoms"; import { blogStaticPaths } from "../demos/blogPosts"; import { complexRoutingStaticPaths } from "../demos/complexRoutingSamples"; +import { switchRoutingStaticPaths } from "../demos/switchRoutingPages"; import { articleSlugs, findArticle } from "../demos/asyncArticles"; import { changelogStaticPaths } from "../pages/changelogEntries"; @@ -32,6 +33,10 @@ export const demosIndexRoute = staticRouteAtom("demos"); export const basicRoutingDemoRoute = staticRouteAtom("basic-routing", { parent: demosIndexRoute }); export const basicRoutingDemoPageRoute = paramRouteAtom("page", { parent: basicRoutingDemoRoute }); +// Switch-statement routing demo: the site's own mount point. The demo reads one page route atom +// and switches on its value, on its own basePath-scoped root inside SwitchRoutingApp. +export const switchRoutingDemoRoute = staticRouteAtom("switch-routing", { parent: demosIndexRoute }); + // Blog routing demo: the site's own mount point. The demo's own /:year/:month/:day/:slug tree // lives inside BlogRoutingApp, on its own basePath-scoped root. export const blogRoutingDemoRoute = staticRouteAtom("blog-routing", { parent: demosIndexRoute }); @@ -67,6 +72,7 @@ const exactRouteMissedAtom = notAtom( demosIndexRoute, basicRoutingDemoRoute, basicRoutingDemoPageRoute, + switchRoutingDemoRoute, blogRoutingDemoRoute, dataGridDemoRoute, complexRoutingDemoRoute, @@ -76,9 +82,9 @@ const exactRouteMissedAtom = notAtom( /** * Whether the current location has nothing behind it, which is what makes a server render's - * *status code* right and not just its HTML. Everything under the changelog's, the blog demo's - * and the complex-routing demo's mounts counts as found - all three route their own subtree and - * render their own not-found views. The async demo gets no such blanket, and lists + * *status code* right and not just its HTML. Everything under the changelog's, the switch demo's, + * the blog demo's and the complex-routing demo's mounts counts as found - all four route their own + * subtree and render their own not-found views. The async demo gets no such blanket, and lists * `asyncArticleRoute` rather than `asyncLookupSlugRoute`: an unknown slug is a genuine miss, even * though the demo page still renders its own not-found view. */ @@ -86,6 +92,7 @@ export const notFoundAtom = atom( (get) => get(exactRouteMissedAtom) && !get(changelogRoute).match && + !get(switchRoutingDemoRoute).match && !get(blogRoutingDemoRoute).match && !get(complexRoutingDemoRoute).match, ); @@ -117,6 +124,7 @@ export const staticPaths: string[] = [ "/demos", "/demos/basic-routing", "/demos/basic-routing/about", + ...switchRoutingStaticPaths(), ...blogStaticPaths(), "/demos/data-grid", ...complexRoutingStaticPaths(),