Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/docs/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
historyRoute,
demosIndexRoute,
basicRoutingDemoRoute,
switchRoutingDemoRoute,
blogRoutingDemoRoute,
dataGridDemoRoute,
complexRoutingDemoRoute,
Expand All @@ -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";
Expand Down Expand Up @@ -63,6 +65,9 @@ export const App = () => (
<Route on={basicRoutingDemoRoute}>
<BasicRoutingDemo />
</Route>
<Route on={switchRoutingDemoRoute}>
<SwitchRoutingDemo />
</Route>
<Route on={blogRoutingDemoRoute}>
<BlogRoutingDemo />
</Route>
Expand Down
113 changes: 113 additions & 0 deletions packages/docs/src/demos/SwitchRoutingApp.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<div>
<h3>Home</h3>
<p>
Nothing here is a <code>&lt;Route&gt;</code>. One route atom holds which page the URL names, and a plain{" "}
<code>switch</code> on that value picks the component to render.
</p>
</div>
);

const AboutPage = () => (
<div>
<h3>About</h3>
<p>
The buttons above navigated here by writing to that same atom &mdash; <code>{`navigate({ page: "about" })`}</code>{" "}
&mdash; rather than by following a <code>&lt;Link&gt;</code>. Writing it set the URL; reading it back is what
moved the switch.
</p>
</div>
);

const ContactPage = () => (
<div>
<h3>Contact</h3>
<p>
Back and forward need nothing extra: the atom derives its value from the location, so the browser&apos;s history
moves the switch exactly the way a button does.
</p>
</div>
);

const NotFoundPage = () => (
<div>
<h3>No such page</h3>
<p>
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.
</p>
</div>
);

const pageView = (page: SwitchRoutingPage | "not-found") => {
switch (page) {
case "home":
return <HomePage />;
case "about":
return <AboutPage />;
case "contact":
return <ContactPage />;
case "not-found":
return <NotFoundPage />;
}
};

const SwitchRoutingNav = ({
current,
onNavigate,
}: {
current: SwitchRoutingPage | "not-found";
onNavigate: (page: SwitchRoutingPage) => void;
}) => (
<nav>
{switchRoutingPages.map((page) => (
<button key={page} type="button" disabled={page === current} onClick={() => onNavigate(page)}>
{switchRoutingPageLabels[page]}
</button>
))}
</nav>
);

/**
* 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 (
<>
<SwitchRoutingNav current={page} onNavigate={(next) => navigate({ page: next })} />
{pageView(page)}
</>
);
};

export default SwitchRoutingApp;
19 changes: 19 additions & 0 deletions packages/docs/src/demos/switchRoutingPages.ts
Original file line number Diff line number Diff line change
@@ -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<SwitchRoutingPage, string> = {
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}`),
];
8 changes: 8 additions & 0 deletions packages/docs/src/pages/DemosIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
blogRoutingDemoRoute,
complexRoutingDemoRoute,
dataGridDemoRoute,
switchRoutingDemoRoute,
} from "../router/routes";

export const DemosIndex = () => (
Expand All @@ -26,6 +27,13 @@ export const DemosIndex = () => (
&mdash; a nested router-within-a-router built from <code>staticRouteAtom</code>/<code>paramRouteAtom</code> and
the atoms-based <code>Link</code>/<code>Route</code> components.
</li>
<li>
<Link route={switchRoutingDemoRoute} to={{}}>
Switch-statement routing
</Link>{" "}
&mdash; the same job with no routing components at all: a <code>switch</code> on one route atom&apos;s value
picks the page, and the nav buttons navigate by writing to that atom instead of rendering a <code>Link</code>.
</li>
<li>
<Link route={blogRoutingDemoRoute} to={{}}>
Blog routing
Expand Down
15 changes: 15 additions & 0 deletions packages/docs/src/pages/SwitchRoutingDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SwitchRoutingApp } from "../demos/SwitchRoutingApp";
import DemoPage from "../lib/DemoPage";
import demoSource from "../demos/SwitchRoutingApp.tsx?raw";

export const SwitchRoutingDemo = () => (
<DemoPage
title="Live demo: routing with a switch statement"
sourcePath="packages/docs/src/demos/SwitchRoutingApp.tsx"
source={demoSource}
>
<SwitchRoutingApp />
</DemoPage>
);

export default SwitchRoutingDemo;
14 changes: 11 additions & 3 deletions packages/docs/src/router/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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 });
Expand Down Expand Up @@ -67,6 +72,7 @@ const exactRouteMissedAtom = notAtom(
demosIndexRoute,
basicRoutingDemoRoute,
basicRoutingDemoPageRoute,
switchRoutingDemoRoute,
blogRoutingDemoRoute,
dataGridDemoRoute,
complexRoutingDemoRoute,
Expand All @@ -76,16 +82,17 @@ 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.
*/
export const notFoundAtom = atom(
(get) =>
get(exactRouteMissedAtom) &&
!get(changelogRoute).match &&
!get(switchRoutingDemoRoute).match &&
!get(blogRoutingDemoRoute).match &&
!get(complexRoutingDemoRoute).match,
);
Expand Down Expand Up @@ -117,6 +124,7 @@ export const staticPaths: string[] = [
"/demos",
"/demos/basic-routing",
"/demos/basic-routing/about",
...switchRoutingStaticPaths(),
...blogStaticPaths(),
"/demos/data-grid",
...complexRoutingStaticPaths(),
Expand Down
Loading