Skip to content

Commit 87df3f1

Browse files
authored
Merge pull request #27 from koordinates/composable-dynamic-routes
2 parents dd434fb + 6f178ce commit 87df3f1

File tree

20 files changed

+520
-401
lines changed

20 files changed

+520
-401
lines changed

commitlint.config.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
module.exports = { extends: ['@commitlint/config-conventional'] };
1+
module.exports = {
2+
extends: ['@commitlint/config-conventional'],
3+
rules: {
4+
'body-max-line-length': [0],
5+
'footer-max-line-length': [0]
6+
}
7+
};

examples/todomvc/routes.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import { createBrowserHistory } from "history";
44
export const history: XstateTreeHistory = createBrowserHistory();
55
const createRoute = buildCreateRoute(history, "/");
66

7-
export const allTodos = createRoute.staticRoute()("/", "SHOW_ALL_TODOS");
8-
export const activeTodos = createRoute.staticRoute()(
9-
"/active",
10-
"SHOW_ACTIVE_TODOS"
11-
);
12-
export const completedTodos = createRoute.staticRoute()(
13-
"/completed",
14-
"SHOW_COMPLETED_TODOS"
15-
);
7+
export const allTodos = createRoute.simpleRoute()({
8+
url: "/",
9+
event: "SHOW_ALL_TODOS",
10+
});
11+
export const activeTodos = createRoute.simpleRoute()({
12+
url: "/active",
13+
event: "SHOW_ACTIVE_TODOS",
14+
});
15+
export const completedTodos = createRoute.simpleRoute()({
16+
url: "/completed",
17+
event: "SHOW_COMPLETED_TODOS",
18+
});
1619

1720
export const routes = [allTodos, activeTodos, completedTodos];

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
"test-examples": "tsc --noEmit",
8282
"todomvc": "vite dev",
8383
"build": "rimraf lib && rimraf out && tsc -p tsconfig.build.json",
84-
"build:watch": "tsc -p tsconfig.build.json -w",
84+
"build:watch": "tsc -p tsconfig.json -w",
8585
"api-extractor": "api-extractor run",
8686
"release": "semantic-release",
8787
"commitlint": "commitlint --edit"

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ export {
2020
type Routing404Event,
2121
type StyledLink,
2222
type ArgumentsForRoute,
23-
type Options,
2423
type Params,
2524
type Query,
2625
type Meta,

src/routing/README.md

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,51 @@ Instead, routing is based around the construction of route objects representing
88

99
First you must build a `createRoute` function, this can be done by calling `buildCreateRoute` exported from xstate-tree. `buildCreateRoute` takes two arguments, a history object and a basePath. These arguments are then stapled to any routes created by the returned `createRoute` function from `buildCreateRoute` so the routes can make use of them. These arguments must match the history and basePath arguments provided to `buildRootComponent`
1010

11-
To construct a Route object you use the `createRoute` function. `createRoute` is a "curried" function, meaning it returns a function when called which requires more arguments. The first argument to `createRoute` is an optional parent route, the arguments to the second function are, url this route handles, event for the route and an options object to define params/query schemas and the meta type
11+
To construct a Route object you can use the `route` function or the `simpleRoute` function. Both are "curried" functions, meaning they return a function when called which requires more arguments. The argument to both is an optional parent route, the argument to the second function is the route options object.
12+
13+
### route
14+
15+
A route gives you full control over the matching and reversing behavior of a route. It's up to you to supply a `matcher` and `reverser` function which control this. The matcher function is supplied the url to match as well as the query string parsed into an object. It then returns either false to indicate no match, or an object containing the extracted params/query data as well as a `matchLength` property which indicates how much of the URL was consumed by this matcher. The passed in URL will always be normalized to start with `/` and end with `/`
16+
17+
The match length is required because matching nested routes start from the highest parent route and matches from parent -> child until either a route doesn't match, or the entire URL is consumed, the route does not match if there is any URL left unconsumed. This also means that routes can't attempt to match the full URL if they are a parent route, ie no using regexes with `$` anchors. If matching the URL with a regex the `matchLength` will be `match[0].length` where `match` is the result of `regex.exec(url)`
18+
19+
The `reverser` function is supplied an object containing `params` if the route defines them, and `query` if the route defines them. `query` can be undefined even if the route provides them because they are only passed to the reverser function for the actual route being reversed, not for any parent routes. The reverser function returns a url representing the given params/query combination.
20+
21+
The other arguments are the event, paramsSchema, querySchema and meta type.
22+
23+
### simpleRoute
24+
25+
Simple route is built on top of `route`, for when you aren't interested in full control of the matcher and reverser functions. It takes the same arguments as `route`, without `matcher`/`reverser` and with an additional `url`. The `url` is a string parsed by [path-to-regexp](https://www.npmjs.com/package/path-to-regexp) to generate the `matcher`/`reverser` functions automatically. Simple routes can be composed with normal routes.
26+
1227

1328
Examples
1429

1530
```typescript
16-
const parentRoute = createRoute.staticRoute()("/foo", "GO_FOO");
17-
const childRoute = createRoute.staticRoute(parentRoute)("/bar/:barId", "GO_BAR", {
18-
params: Z.object({
31+
// In practice you would always use a simpleRoute for this, this is just to show how `route` works
32+
const parentRoute = createRoute.route()({
33+
event: "GO_FOO",
34+
matcher: (url) => {
35+
if (url === "/foo/") {
36+
return {
37+
matchLength: 5,
38+
};
39+
}
40+
41+
return false;
42+
},
43+
reverser: () => "/foo/",
44+
});
45+
const childRoute = createRoute.simpleRoute(parentRoute)({
46+
url: "/bar/:barId",
47+
event: "GO_BAR",
48+
paramsSchema: Z.object({
1949
barId: Z.string()
2050
})
2151
});
2252

23-
const routeWithMeta = createRoute.staticRoute()("/whatever", "GO_WHATEVER", {
53+
const routeWithMeta = createRoute.simpleRoute()({
54+
url: "/whatever",
55+
event: "GO_WHATEVER",
2456
meta: {} as { metaField: string }
2557
});
2658
```
@@ -70,9 +102,9 @@ It is done this way so that you don't need to have handlers at every layer of th
70102
How this works in practice is like so, given the following routes
71103

72104
```typescript
73-
const topRoute = createRoute.staticRoute()("/foo", "GO_FOO");
74-
const middleRoute = createRoute.staticRoute(topRoute)("/bar", "GO_BAR");
75-
const bottomRoute = createRoute.staticRoute(middleRoute)("/qux", "GO_QUX");
105+
const topRoute = createRoute.simpleROute()({ url: "/foo", event: "GO_FOO" });
106+
const middleRoute = createRoute.staticRoute(topRoute)({ url: "/bar", event: "GO_BAR" });
107+
const bottomRoute = createRoute.staticRoute(middleRoute)({ url: "/qux", event: "GO_QUX" });
76108
```
77109

78110
if you were to load up the URL `/foo/bar/qux` which is matched by the `bottomRoute` the following happens
@@ -114,10 +146,12 @@ These by default return window.location.pathname and window.location.search whic
114146
### A full example
115147

116148
```typescript
117-
const home = createRoute.staticRoute()("/", "GO_HOME");
118-
const products = createRoute.staticRoute()("/products", "GO_PRODUCTS");
119-
const product = createRoute.staticRoute(products)("/:productId(\\d+)", "GO_PRODUCT", {
120-
params: Z.object({
149+
const home = createRoute.simpleRoute()({ url: "/", event: "GO_HOME" });
150+
const products = createRoute.simpleRoute()({ url: "/products", event: "GO_PRODUCTS" });
151+
const product = createRoute.simpleRoute(products)({
152+
url: "/:productId(\\d+)",
153+
event: "GO_PRODUCT",
154+
paramsSchema: Z.object({
121155
// All params come in as strings, but this actually represents a number so transform it into one
122156
productId: Z.string().transform((id) => parseInt(id, 10))
123157
})

0 commit comments

Comments
 (0)