From a2a711024f06cb44b37819050802094e8fdcee1a Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 13 Sep 2022 14:23:58 -0400 Subject: [PATCH 1/6] feat: Support optional params in route matching --- .../__tests__/path-matching-test.tsx | 90 ++++++++++++++++++- packages/router/utils.ts | 38 +++++++- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/packages/react-router/__tests__/path-matching-test.tsx b/packages/react-router/__tests__/path-matching-test.tsx index dd5e58a3af..03553ccae3 100644 --- a/packages/react-router/__tests__/path-matching-test.tsx +++ b/packages/react-router/__tests__/path-matching-test.tsx @@ -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" }]; @@ -251,20 +259,96 @@ 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 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/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" }, + }, + ]); + }); +}); diff --git a/packages/router/utils.ts b/packages/router/utils.ts index d9ce7d3242..767074dcaf 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -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; From 50d99a6671a2a1fc1ee99b1861d761034fcf90d0 Mon Sep 17 00:00:00 2001 From: Daniel Rios Pavia Date: Fri, 25 Nov 2022 11:23:58 +0100 Subject: [PATCH 2/6] feat: optional static segments --- .../__tests__/path-matching-test.tsx | 76 +++++++++++++++++++ packages/router/utils.ts | 4 +- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/packages/react-router/__tests__/path-matching-test.tsx b/packages/react-router/__tests__/path-matching-test.tsx index 03553ccae3..9a27c3a39e 100644 --- a/packages/react-router/__tests__/path-matching-test.tsx +++ b/packages/react-router/__tests__/path-matching-test.tsx @@ -277,6 +277,82 @@ describe("path matching with splats", () => { }); }); +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/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 = [ diff --git a/packages/router/utils.ts b/packages/router/utils.ts index 767074dcaf..72699b4ee1 100644 --- a/packages/router/utils.ts +++ b/packages/router/utils.ts @@ -419,7 +419,7 @@ function flattenRoutes< let segments = path.split("/"); let optionalParams: string[] = []; segments.forEach((segment) => { - let match = segment.match(/^:([^?]+)\?$/); + let match = segment.match(/^:?([^?]+)\?$/); if (match) { optionalParams.push(match[1]); } @@ -431,7 +431,7 @@ function flattenRoutes< let newMeta = routesMeta.map((m) => ({ ...m })); for (let j = optionalParams.length - 1; j >= 0; j--) { - let re = new RegExp(`(\\/:${optionalParams[j]})\\?`); + let re = new RegExp(`(\\/:?${optionalParams[j]})\\?`); let replacement = j < i ? "$1" : ""; newPath = newPath.replace(re, replacement); newMeta[newMeta.length - 1].relativePath = newMeta[ From 1ed25edc17faa9cbd260d501aedc6c6bdfcd6406 Mon Sep 17 00:00:00 2001 From: Daniel Rios Pavia Date: Tue, 29 Nov 2022 11:41:58 +0100 Subject: [PATCH 3/6] Add to contributors --- contributors.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/contributors.yml b/contributors.yml index aa40f67154..091718e2c1 100644 --- a/contributors.yml +++ b/contributors.yml @@ -76,6 +76,7 @@ - KostiantynPopovych - KutnerUri - latin-1 +- lordofthecactus - liuhanqu - loun4 - lqze From 13b559345117c5ece2416c318c73b424bdb98a5f Mon Sep 17 00:00:00 2001 From: Daniel Rios Pavia Date: Tue, 29 Nov 2022 12:00:45 +0100 Subject: [PATCH 4/6] Add changeset --- .changeset/new-taxis-stare.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .changeset/new-taxis-stare.md diff --git a/.changeset/new-taxis-stare.md b/.changeset/new-taxis-stare.md new file mode 100644 index 0000000000..b3085a58cd --- /dev/null +++ b/.changeset/new-taxis-stare.md @@ -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 +``` From d7883825aabda56b598edbc69498920075ff9579 Mon Sep 17 00:00:00 2001 From: Daniel Rios Pavia Date: Wed, 30 Nov 2022 11:14:22 +0100 Subject: [PATCH 5/6] feat(optional-segments): add missing patch matching tests --- .../react-router/__tests__/path-matching-test.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/react-router/__tests__/path-matching-test.tsx b/packages/react-router/__tests__/path-matching-test.tsx index 9a27c3a39e..4f4a4ad32e 100644 --- a/packages/react-router/__tests__/path-matching-test.tsx +++ b/packages/react-router/__tests__/path-matching-test.tsx @@ -338,6 +338,12 @@ describe("path matchine with optional segments", () => { 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?", @@ -414,6 +420,12 @@ describe("path matching with optional dynamic segments", () => { 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?", From afbcf9af32ccfc6be022d481873a191df07feb82 Mon Sep 17 00:00:00 2001 From: Daniel Rios Pavia Date: Wed, 30 Nov 2022 11:15:47 +0100 Subject: [PATCH 6/6] feat(optional-segments): update router.umd.min.js filesize --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d359a2a285..d7b6afc2db 100644 --- a/package.json +++ b/package.json @@ -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"