From 4a522e7a3934fdf2f3ae47ffde760755edd8d734 Mon Sep 17 00:00:00 2001 From: Taylor Lodge Date: Tue, 9 Jan 2024 15:57:30 +1300 Subject: [PATCH] feat(builders): buildRoutingMachine Utility function to reduce boilerplate for machines that solely consist of event -> state -> invoke machine states Closes #50 --- src/builders.spec.tsx | 45 +++++++++++++++++++- src/{builders.ts => builders.tsx} | 70 ++++++++++++++++++++++++++++++- src/routing/Link.spec.tsx | 4 +- xstate-tree.api.md | 3 ++ 4 files changed, 116 insertions(+), 6 deletions(-) rename src/{builders.ts => builders.tsx} (82%) diff --git a/src/builders.spec.tsx b/src/builders.spec.tsx index c0be825..03b03c8 100644 --- a/src/builders.spec.tsx +++ b/src/builders.spec.tsx @@ -1,7 +1,10 @@ -import { render, waitFor } from "@testing-library/react"; +import { act, render, waitFor } from "@testing-library/react"; +import { createMemoryHistory } from "history"; import React from "react"; -import { viewToMachine } from "./builders"; +import { buildRoutingMachine, viewToMachine } from "./builders"; +import { buildCreateRoute } from "./routing"; +import { XstateTreeHistory } from "./types"; import { buildRootComponent } from "./xstateTree"; describe("xstate-tree builders", () => { @@ -15,4 +18,42 @@ describe("xstate-tree builders", () => { await waitFor(() => getByText("hello world")); }); }); + + describe("buildRoutingMachine", () => { + const hist: XstateTreeHistory = createMemoryHistory(); + const createRoute = buildCreateRoute(() => hist, "/"); + + const fooRoute = createRoute.simpleRoute()({ + url: "/foo/", + event: "GO_TO_FOO", + }); + const barRoute = createRoute.simpleRoute()({ + url: "/bar/", + event: "GO_TO_BAR", + }); + + it("takes a mapping of routes to machines and returns a machine that invokes those machines when those routes events are broadcast", async () => { + const FooMachine = viewToMachine(() =>
foo
); + const BarMachine = viewToMachine(() =>
bar
); + + const routingMachine = buildRoutingMachine([fooRoute, barRoute], { + GO_TO_FOO: FooMachine, + GO_TO_BAR: BarMachine, + }); + + const Root = buildRootComponent(routingMachine, { + history: hist, + basePath: "/", + routes: [fooRoute, barRoute], + }); + + const { getByText } = render(); + + act(() => fooRoute.navigate()); + await waitFor(() => getByText("foo")); + + act(() => barRoute.navigate()); + await waitFor(() => getByText("bar")); + }); + }); }); diff --git a/src/builders.ts b/src/builders.tsx similarity index 82% rename from src/builders.ts rename to src/builders.tsx index 319d44d..7ecdb9d 100644 --- a/src/builders.ts +++ b/src/builders.tsx @@ -8,9 +8,11 @@ import { type InterpreterFrom, type AnyFunction, createMachine, + StateNodeConfig, } from "xstate"; -import { Slot } from "./slots"; +import { AnyRoute } from "./routing"; +import { Slot, singleSlot } from "./slots"; import { AnyActions, AnySelector, @@ -274,9 +276,73 @@ export function createXStateTreeMachine< */ export function viewToMachine(view: () => JSX.Element): AnyXstateTreeMachine { return createXStateTreeMachine( - createMachine({ initial: "idle", states: { idle: {} } }), + createMachine({ + initial: "idle", + states: { idle: {} }, + }), { View: view, } ); } + +/** + * @public + * + * Utility to aid in reducing boilerplate of mapping route events to xstate-tree machines + * + * Takes a list of routes and a mapping of route events to xstate-tree machines and returns an xstate-tree machine + * that renders the machines based on the routing events + * + * @param _routes - the array of routes you wish to map to machines + * @param mappings - an object mapping the route events to the machine to invoke + * @returns an xstate-tree machine that will render the right machines based on the routing events + */ +export function buildRoutingMachine( + _routes: TRoutes, + mappings: Record +): AnyXstateTreeMachine { + const contentSlot = singleSlot("Content"); + const mappingsToStates = Object.entries( + mappings + ).reduce((acc, [event, _machine]) => { + return { + ...acc, + [event]: { + invoke: { + src: (_ctx, e) => { + return mappings[e.type as TRoutes[number]["event"]]; + }, + id: contentSlot.getId(), + }, + }, + }; + }, {} as Record>); + + const mappingsToEvents = Object.keys(mappings).reduce( + (acc, event) => ({ + ...acc, + [event]: { + target: `.${event}`, + }, + }), + {} + ); + const machine = createMachine({ + on: { + ...mappingsToEvents, + }, + initial: "idle", + states: { + idle: {}, + ...mappingsToStates, + }, + }); + + return createXStateTreeMachine(machine, { + slots: [contentSlot], + View: ({ slots }) => { + return ; + }, + }); +} diff --git a/src/routing/Link.spec.tsx b/src/routing/Link.spec.tsx index 92b8c28..c7168a9 100644 --- a/src/routing/Link.spec.tsx +++ b/src/routing/Link.spec.tsx @@ -69,7 +69,7 @@ describe("Link", () => { route.preload = jest.fn(); const { getByText } = render( - + Link ); @@ -78,7 +78,7 @@ describe("Link", () => { await delay(2); await userEvent.unhover(getByText("Link")); - await delay(15); + await delay(50); expect(route.preload).not.toHaveBeenCalled(); }); diff --git a/xstate-tree.api.md b/xstate-tree.api.md index 0a7aa3d..fffb35e 100644 --- a/xstate-tree.api.md +++ b/xstate-tree.api.md @@ -126,6 +126,9 @@ export function buildRootComponent(machine: AnyXstateTreeMachine, routing?: { rootMachine: AnyXstateTreeMachine; }; +// @public +export function buildRoutingMachine(_routes: TRoutes, mappings: Record): AnyXstateTreeMachine; + // Warning: (ae-incompatible-release-tags) The symbol "buildSelectors" is marked as @public, but its signature references "CanHandleEvent" which is marked as @internal // Warning: (ae-incompatible-release-tags) The symbol "buildSelectors" is marked as @public, but its signature references "MatchesFrom" which is marked as @internal //