Skip to content

Commit

Permalink
Merge pull request #9650 from lordofthecactus/optional-params
Browse files Browse the repository at this point in the history
feat: optional params and static segments
  • Loading branch information
pcattori committed Nov 30, 2022
2 parents 0729641 + afbcf9a commit 44b6af7
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 5 deletions.
34 changes: 34 additions & 0 deletions .changeset/new-taxis-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"react-router": minor
"@remix-run/router": minor
---

Allows optional routes and optional static segments

**Optional params examples**

`:lang?/about` will get expanded matched with

```
/:lang/about
/about
```

`/multistep/:widget1?/widget2?/widget3?`
Will get expanded matched with:

```
/multistep
/multistep/:widget1
/multistep/:widget1/:widget2
/multistep/:widget1/:widget2/:widget3
```

**optional static segment example**

`/fr?/about` will get expanded and matched with:

```
/about
/fr/about
```
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
- KostiantynPopovych
- KutnerUri
- latin-1
- lordofthecactus
- liuhanqu
- loun4
- lqze
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "35 kB"
"none": "35.5 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "12.5 kB"
Expand Down
178 changes: 175 additions & 3 deletions packages/react-router/__tests__/path-matching-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ function pickPaths(routes: RouteObject[], pathname: string): string[] | null {
return matches && matches.map((match) => match.route.path || "");
}

function pickPathsAndParams(routes: RouteObject[], pathname: string) {
let matches = matchRoutes(routes, pathname);
return (
matches &&
matches.map((match) => ({ path: match.route.path, params: match.params }))
);
}

describe("path matching", () => {
test("root vs. dynamic", () => {
let routes = [{ path: "/" }, { path: ":id" }];
Expand Down Expand Up @@ -251,20 +259,184 @@ describe("path matching with splats", () => {

expect(match).not.toBeNull();
expect(match).toHaveLength(3);
expect(match[0]).toMatchObject({
expect(match![0]).toMatchObject({
params: { "*": "abc" },
pathname: "/",
pathnameBase: "/",
});
expect(match[1]).toMatchObject({
expect(match![1]).toMatchObject({
params: { "*": "abc" },
pathname: "/courses",
pathnameBase: "/courses",
});
expect(match[2]).toMatchObject({
expect(match![2]).toMatchObject({
params: { "*": "abc" },
pathname: "/courses/abc",
pathnameBase: "/courses",
});
});
});

describe("path matchine with optional segments", () => {
test("optional static segment at the start of the path", () => {
let routes = [
{
path: "/en?/abc",
},
];

expect(pickPathsAndParams(routes, "/")).toEqual(null);
expect(pickPathsAndParams(routes, "/abc")).toEqual([
{
path: "/en?/abc",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/en/abc")).toEqual([
{
path: "/en?/abc",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/en/abc/bar")).toEqual(null);
});

test("optional static segment at the end of the path", () => {
let routes = [
{
path: "/nested/one?/two?",
},
];

expect(pickPathsAndParams(routes, "/nested")).toEqual([
{
path: "/nested/one?/two?",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/nested/one")).toEqual([
{
path: "/nested/one?/two?",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/nested/one/two")).toEqual([
{
path: "/nested/one?/two?",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/nested/one/two/baz")).toEqual(null);
});

test("intercalated optional static segments", () => {
let routes = [
{
path: "/nested/one?/two/three?",
},
];

expect(pickPathsAndParams(routes, "/nested")).toEqual(null);
expect(pickPathsAndParams(routes, "/nested/one")).toEqual(null);
expect(pickPathsAndParams(routes, "/nested/two")).toEqual([
{
path: "/nested/one?/two/three?",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/nested/one/two")).toEqual([
{
path: "/nested/one?/two/three?",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/nested/one/two/three")).toEqual([
{
path: "/nested/one?/two/three?",
params: {},
},
]);
});
});

describe("path matching with optional dynamic segments", () => {
test("optional params at the start of the path", () => {
let routes = [
{
path: "/:lang?/abc",
},
];

expect(pickPathsAndParams(routes, "/")).toEqual(null);
expect(pickPathsAndParams(routes, "/abc")).toEqual([
{
path: "/:lang?/abc",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/en/abc")).toEqual([
{
path: "/:lang?/abc",
params: { lang: "en" },
},
]);
expect(pickPathsAndParams(routes, "/en/abc/bar")).toEqual(null);
});

test("optional params at the end of the path", () => {
let routes = [
{
path: "/nested/:one?/:two?",
},
];

expect(pickPathsAndParams(routes, "/nested")).toEqual([
{
path: "/nested/:one?/:two?",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/nested/foo")).toEqual([
{
path: "/nested/:one?/:two?",
params: { one: "foo" },
},
]);
expect(pickPathsAndParams(routes, "/nested/foo/bar")).toEqual([
{
path: "/nested/:one?/:two?",
params: { one: "foo", two: "bar" },
},
]);
expect(pickPathsAndParams(routes, "/nested/foo/bar/baz")).toEqual(null);
});

test("intercalated optional params", () => {
let routes = [
{
path: "/nested/:one?/two/:three?",
},
];

expect(pickPathsAndParams(routes, "/nested")).toEqual(null);
expect(pickPathsAndParams(routes, "/nested/foo")).toEqual(null);
expect(pickPathsAndParams(routes, "/nested/two")).toEqual([
{
path: "/nested/:one?/two/:three?",
params: {},
},
]);
expect(pickPathsAndParams(routes, "/nested/foo/two")).toEqual([
{
path: "/nested/:one?/two/:three?",
params: { one: "foo" },
},
]);
expect(pickPathsAndParams(routes, "/nested/foo/two/bar")).toEqual([
{
path: "/nested/:one?/two/:three?",
params: { one: "foo", three: "bar" },
},
]);
});
});
38 changes: 37 additions & 1 deletion packages/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,43 @@ function flattenRoutes<
return;
}

branches.push({ path, score: computeScore(path, route.index), routesMeta });
// Handle optional params - /path/:optional?
let segments = path.split("/");
let optionalParams: string[] = [];
segments.forEach((segment) => {
let match = segment.match(/^:?([^?]+)\?$/);
if (match) {
optionalParams.push(match[1]);
}
});

if (optionalParams.length > 0) {
for (let i = 0; i <= optionalParams.length; i++) {
let newPath = path;
let newMeta = routesMeta.map((m) => ({ ...m }));

for (let j = optionalParams.length - 1; j >= 0; j--) {
let re = new RegExp(`(\\/:?${optionalParams[j]})\\?`);
let replacement = j < i ? "$1" : "";
newPath = newPath.replace(re, replacement);
newMeta[newMeta.length - 1].relativePath = newMeta[
newMeta.length - 1
].relativePath.replace(re, replacement);
}

branches.push({
path: newPath,
score: computeScore(newPath, route.index),
routesMeta: newMeta,
});
}
} else {
branches.push({
path,
score: computeScore(path, route.index),
routesMeta,
});
}
});

return branches;
Expand Down

0 comments on commit 44b6af7

Please sign in to comment.