Skip to content

Commit

Permalink
feat(builders): buildRoutingMachine
Browse files Browse the repository at this point in the history
Utility function to reduce boilerplate for machines that solely consist of event -> state -> invoke machine states

Closes #50
  • Loading branch information
UberMouse committed Jan 9, 2024
1 parent 68a748d commit fb94a1f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 5 deletions.
45 changes: 43 additions & 2 deletions src/builders.spec.tsx
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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(() => <div>foo</div>);
const BarMachine = viewToMachine(() => <div>bar</div>);

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(<Root />);

act(() => fooRoute.navigate());
await waitFor(() => getByText("foo"));

act(() => barRoute.navigate());
await waitFor(() => getByText("bar"));
});
});
});
70 changes: 68 additions & 2 deletions src/builders.ts → src/builders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<TRoutes extends AnyRoute[]>(
_routes: TRoutes,
mappings: Record<TRoutes[number]["event"], AnyXstateTreeMachine>
): AnyXstateTreeMachine {
const contentSlot = singleSlot("Content");
const mappingsToStates = Object.entries<AnyXstateTreeMachine>(
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<string, StateNodeConfig<any, any, any>>);

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 <slots.Content />;
},
});
}
2 changes: 1 addition & 1 deletion src/routing/Link.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ describe("Link", () => {
route.preload = jest.fn();

const { getByText } = render(
<Link to={route} params={{ param: "test" }} preloadOnHoverMs={15}>
<Link to={route} params={{ param: "test" }} preloadOnHoverMs={150}>
Link
</Link>
);
Expand Down
3 changes: 3 additions & 0 deletions xstate-tree.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ export function buildRootComponent(machine: AnyXstateTreeMachine, routing?: {
rootMachine: AnyXstateTreeMachine;
};

// @public
export function buildRoutingMachine<TRoutes extends AnyRoute[]>(_routes: TRoutes, mappings: Record<TRoutes[number]["event"], AnyXstateTreeMachine>): 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
//
Expand Down

0 comments on commit fb94a1f

Please sign in to comment.