Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(builders): buildRoutingMachine #55

Merged
merged 1 commit into from
Jan 9, 2024
Merged
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
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 />;
},
});
}
4 changes: 2 additions & 2 deletions 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={50}>
Link
</Link>
);
Expand All @@ -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();
});

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
Loading