@solidjs/router 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  A router lets you change your view based on the URL in the browser. This allows your "single-page" application to simulate a traditional multipage site. To use Solid Router, you specify components called Routes that depend on the value of the URL (the "path"), and the router handles the mechanism of swapping them in and out.
8
8
 
9
- Solid Router is a universal router for SolidJS - it works whether you're rendering on the client or on the server. It was inspired by and combines paradigms of React Router and the Ember Router. Routes can be defined directly in your app's template using JSX, but you can also pass your route configuration directly as an object. It also supports nested routing, so navigation can change a part of a component, rather than completely replacing it.
9
+ Solid Router is a universal router for SolidJS - it works whether you're rendering on the client or on the server. It was inspired by and combines paradigms of React Router and the Ember Router. Routes can be defined directly in your app's template using JSX, but you can also pass your route configuration directly as an object. It also supports nested routing, so navigation can change a part of a component, rather than completely replacing it.
10
10
 
11
11
  It supports all of Solid's SSR methods and has Solid's transitions baked in, so use it freely with suspense, resources, and lazy components. Solid Router also allows you to define a data function that loads parallel to the routes ([render-as-you-fetch](https://epicreact.dev/render-as-you-fetch/)).
12
12
 
@@ -16,8 +16,8 @@ It supports all of Solid's SSR methods and has Solid's transitions baked in, so
16
16
  - [Create Links to Your Routes](#create-links-to-your-routes)
17
17
  - [Dynamic Routes](#dynamic-routes)
18
18
  - [Data Functions](#data-functions)
19
- - [Nested Routes](#nested-routes)
20
- - [Hash Mode Router](#hash-mode-router)
19
+ - [Nested Routes](#nested-routes)
20
+ - [Hash Mode Router](#hash-mode-router)
21
21
  - [Config Based Routing](#config-based-routing)
22
22
  - [Router Primitives](#router-primitives)
23
23
  - [useParams](#useparams)
@@ -63,17 +63,16 @@ Solid Router allows you to configure your routes using JSX:
63
63
 
64
64
  1. Use the `Routes` component to specify where the routes should appear in your app.
65
65
 
66
-
67
66
  ```jsx
68
67
  import { Routes, Route } from "@solidjs/router"
69
68
 
70
69
  export default function App() {
71
70
  return <>
72
- <h1>My Site with Lots of Pages</h1>
71
+ <h1>My Site with Lots of Pages</h1>
73
72
  <Routes>
74
73
 
75
74
  </Routes>
76
- </>
75
+ </>
77
76
  }
78
77
  ```
79
78
 
@@ -87,13 +86,13 @@ import Users from "./pages/Users"
87
86
 
88
87
  export default function App() {
89
88
  return <>
90
- <h1>My Site with Lots of Pages</h1>
91
- <Routes>
92
- <Route path="/users" component={Users} />
93
- <Route path="/" component={Home} />
94
- <Route path="/about" element={<div>This site was made with Solid</div>} />
95
- </Routes>
96
- </>
89
+ <h1>My Site with Lots of Pages</h1>
90
+ <Routes>
91
+ <Route path="/users" component={Users} />
92
+ <Route path="/" component={Home} />
93
+ <Route path="/about" element={<div>This site was made with Solid</div>} />
94
+ </Routes>
95
+ </>
97
96
  }
98
97
  ```
99
98
 
@@ -109,13 +108,13 @@ const Home = lazy(() => import("./pages/Home"));
109
108
 
110
109
  export default function App() {
111
110
  return <>
112
- <h1>My Site with Lots of Pages</h1>
113
- <Routes>
114
- <Route path="/users" component={Users} />
115
- <Route path="/" component={Home} />
116
- <Route path="/about" element={<div>This site was made with Solid</div>} />
117
- </Routes>
118
- </>
111
+ <h1>My Site with Lots of Pages</h1>
112
+ <Routes>
113
+ <Route path="/users" component={Users} />
114
+ <Route path="/" component={Home} />
115
+ <Route path="/about" element={<div>This site was made with Solid</div>} />
116
+ </Routes>
117
+ </>
119
118
  }
120
119
  ```
121
120
 
@@ -131,17 +130,17 @@ const Home = lazy(() => import("./pages/Home"));
131
130
 
132
131
  export default function App() {
133
132
  return <>
134
- <h1>My Site with Lots of Pages</h1>
135
- <nav>
136
- <A href="/about">About</A>
137
- <A href="/">Home</A>
138
- </nav>
139
- <Routes>
140
- <Route path="/users" component={Users} />
141
- <Route path="/" component={Home} />
142
- <Route path="/about" element={<div>This site was made with Solid</div>} />
143
- </Routes>
144
- </>
133
+ <h1>My Site with Lots of Pages</h1>
134
+ <nav>
135
+ <A href="/about">About</A>
136
+ <A href="/">Home</A>
137
+ </nav>
138
+ <Routes>
139
+ <Route path="/users" component={Users} />
140
+ <Route path="/" component={Home} />
141
+ <Route path="/about" element={<div>This site was made with Solid</div>} />
142
+ </Routes>
143
+ </>
145
144
  }
146
145
  ```
147
146
 
@@ -163,7 +162,7 @@ Solid Router provides a `Navigate` component that works similarly to `A`, but it
163
162
 
164
163
  ```jsx
165
164
  function getPath ({navigate, location}) {
166
- //navigate is the result of calling useNavigate(); location is the result of calling useLocation().
165
+ //navigate is the result of calling useNavigate(); location is the result of calling useLocation().
167
166
  //You can use those to dynamically determine a path to navigate to
168
167
  return "/some-path";
169
168
  }
@@ -174,7 +173,7 @@ function getPath ({navigate, location}) {
174
173
 
175
174
  ## Dynamic Routes
176
175
 
177
- If you don't know the path ahead of time, you might want to treat part of the path as a flexible parameter that is passed on to the component.
176
+ If you don't know the path ahead of time, you might want to treat part of the path as a flexible parameter that is passed on to the component.
178
177
 
179
178
  ```jsx
180
179
  import { lazy } from "solid-js";
@@ -185,14 +184,14 @@ const Home = lazy(() => import("./pages/Home"));
185
184
 
186
185
  export default function App() {
187
186
  return <>
188
- <h1>My Site with Lots of Pages</h1>
189
- <Routes>
190
- <Route path="/users" component={Users} />
191
- <Route path="/users/:id" component={User} />
192
- <Route path="/" component={Home} />
193
- <Route path="/about" element={<div>This site was made with Solid</div>} />
194
- </Routes>
195
- </>
187
+ <h1>My Site with Lots of Pages</h1>
188
+ <Routes>
189
+ <Route path="/users" component={Users} />
190
+ <Route path="/users/:id" component={User} />
191
+ <Route path="/" component={Home} />
192
+ <Route path="/about" element={<div>This site was made with Solid</div>} />
193
+ </Routes>
194
+ </>
196
195
  }
197
196
  ```
198
197
 
@@ -200,6 +199,48 @@ The colon indicates that `id` can be any string, and as long as the URL fits tha
200
199
 
201
200
  You can then access that `id` from within a route component with `useParams`:
202
201
 
202
+ ---
203
+
204
+ Each path parameter can be validated using a `MatchFilter`.
205
+ This allows for more complex routing descriptions than just checking the presence of a parameter.
206
+
207
+ ```tsx
208
+ import {lazy} from "solid-js";
209
+ import {Routes, Route} from "@solidjs/router"
210
+ import type {SegmentValidators} from "./types";
211
+
212
+ const Users = lazy(() => import("./pages/Users"));
213
+ const User = lazy(() => import("./pages/User"));
214
+ const Home = lazy(() => import("./pages/Home"));
215
+
216
+ const filters: MatchFilters = {
217
+ parent: ['mom', 'dad'], // allow enum values
218
+ id: /^\d+$/, // only allow numbers
219
+ withHtmlExtension: (v: string) => v.length > 5 && v.endsWith('.html') // we want an `*.html` extension
220
+ }
221
+
222
+ export default function App() {
223
+ return <>
224
+ <h1>My Site with Lots of Pages</h1>
225
+ <Routes>
226
+ <Route path="/users/:parent/:id/:withHtmlExtension" component={User} matchFilters={filters}/>
227
+ </Routes>
228
+ </>
229
+ }
230
+ ```
231
+
232
+ Here, we have added the `matchFilters` prop. This allows us to validate the `parent`, `id` and `withHtmlExtension` parameters against the filters defined in `filters`.
233
+ If the validation fails, the route will not match.
234
+
235
+ So in this example:
236
+
237
+ - `/users/mom/123/contact.html` would match,
238
+ - `/users/dad/123/about.html` would match,
239
+ - `/users/aunt/123/contact.html` would not match as `:parent` is not 'mom' or 'dad',
240
+ - `/users/mom/me/contact.html` would not match as `:id` is not a number,
241
+ - `/users/dad/123/contact` would not match as `:withHtmlExtension` is missing `.html`.
242
+
243
+ ---
203
244
 
204
245
  ```jsx
205
246
  //async fetching function
@@ -250,12 +291,10 @@ Routes also support defining multiple paths using an array. This allows a route
250
291
  <Route path={["login", "register"]} component={Login}/>
251
292
  ```
252
293
 
253
-
254
294
  ## Data Functions
255
295
  In the [above example](#dynamic-routes), the User component is lazy-loaded and then the data is fetched. With route data functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible.
256
296
 
257
- To do this, create a function that fetches and returns the data using `createResource`. Then pass that function to the `data` prop of the `Route` component.
258
-
297
+ To do this, create a function that fetches and returns the data using `createResource`. Then pass that function to the `data` prop of the `Route` component.
259
298
 
260
299
  ```js
261
300
  import { lazy } from "solid-js";
@@ -299,7 +338,7 @@ A common pattern is to export the data function that corresponds to a route in a
299
338
  ```js
300
339
  import { lazy } from "solid-js";
301
340
  import { Route } from "@solidjs/router";
302
- import { fetchUser } ...
341
+ import { fetchUser } ...
303
342
  import UserData from "./pages/users/[id].data.js";
304
343
  const User = lazy(() => import("/pages/users/[id].js"));
305
344
 
@@ -341,15 +380,14 @@ Only leaf Route nodes (innermost `Route` components) are given a route. If you w
341
380
 
342
381
  You can also take advantage of nesting by adding a parent element with an `<Outlet/>`.
343
382
  ```jsx
344
-
345
383
  import { Outlet } from "@solidjs/router";
346
384
 
347
385
  function PageWrapper () {
348
386
  return <div>
349
- <h1> We love our users! </h1>
387
+ <h1> We love our users! </h1>
350
388
  <Outlet/>
351
- <A href="/">Back Home</A>
352
- </div>
389
+ <A href="/">Back Home</A>
390
+ </div>
353
391
  }
354
392
 
355
393
  <Route path="/users" component={PageWrapper}>
@@ -369,7 +407,7 @@ You can nest indefinitely - just remember that only leaf nodes will become their
369
407
  </Route>
370
408
  ```
371
409
 
372
- If you declare a `data` function on a parent and a child, the result of the parent's data function will be passed to the child's data function as the `data` property of the argument, as described in the last section. This works even if it isn't a direct child, because by default every route forwards its parent's data.
410
+ If you declare a `data` function on a parent and a child, the result of the parent's data function will be passed to the child's data function as the `data` property of the argument, as described in the last section. This works even if it isn't a direct child, because by default every route forwards its parent's data.
373
411
 
374
412
  ## Hash Mode Router
375
413
 
@@ -378,7 +416,7 @@ By default, Solid Router uses `location.pathname` as route path. You can simply
378
416
  ```jsx
379
417
  import { Router, hashIntegration } from '@solidjs/router'
380
418
 
381
- <Router source={hashIntegration()}><App></Router>
419
+ <Router source={hashIntegration()}><App /></Router>
382
420
  ```
383
421
 
384
422
  ## Config Based Routing
@@ -495,7 +533,7 @@ const [searchParams, setSearchParams] = useSearchParams();
495
533
  return (
496
534
  <div>
497
535
  <span>Page: {searchParams.page}</span>
498
- <button onClick={() => setSearchParams({ page: searchParams.page + 1 })}>Next Page</button>
536
+ <button onClick={() => setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })}>Next Page</button>
499
537
  </div>
500
538
  );
501
539
  ```
@@ -560,11 +598,9 @@ useBeforeLeave((e: BeforeLeaveEventArgs) => {
560
598
  setTimeout(() => {
561
599
  if (window.confirm("Discard unsaved changes - are you sure?")) {
562
600
  // user wants to proceed anyway so retry with force=true
563
- e.retry(true);
601
+ e.retry(true);
564
602
  }
565
603
  }, 100);
566
604
  }
567
605
  });
568
606
  ```
569
-
570
-
@@ -1,5 +1,5 @@
1
1
  import type { Component, JSX } from "solid-js";
2
- import type { Location, LocationChangeSignal, Navigator, RouteDataFunc, RouteDefinition, RouterIntegration } from "./types";
2
+ import type { Location, LocationChangeSignal, MatchFilters, Navigator, RouteDataFunc, RouteDefinition, RouterIntegration } from "./types";
3
3
  declare module "solid-js" {
4
4
  namespace JSX {
5
5
  interface AnchorHTMLAttributes<T> {
@@ -29,10 +29,11 @@ export interface RoutesProps {
29
29
  }
30
30
  export declare const Routes: (props: RoutesProps) => JSX.Element;
31
31
  export declare const useRoutes: (routes: RouteDefinition | RouteDefinition[], base?: string) => () => JSX.Element;
32
- export type RouteProps = {
33
- path: string | string[];
32
+ export type RouteProps<S extends string> = {
33
+ path: S | S[];
34
34
  children?: JSX.Element;
35
35
  data?: RouteDataFunc;
36
+ matchFilters?: MatchFilters<S>;
36
37
  } & ({
37
38
  element?: never;
38
39
  component: Component;
@@ -41,7 +42,7 @@ export type RouteProps = {
41
42
  element?: JSX.Element;
42
43
  preload?: () => void;
43
44
  });
44
- export declare const Route: (props: RouteProps) => JSX.Element;
45
+ export declare const Route: <S extends string>(props: RouteProps<S>) => JSX.Element;
45
46
  export declare const Outlet: () => JSX.Element;
46
47
  export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "state"> {
47
48
  href: string;
@@ -3,7 +3,7 @@ import { children, createMemo, createRoot, mergeProps, on, Show, splitProps } fr
3
3
  import { isServer } from "solid-js/web";
4
4
  import { pathIntegration, staticIntegration } from "./integration";
5
5
  import { createBranches, createRouteContext, createRouterContext, getRouteMatches, RouteContextObj, RouterContextObj, useHref, useLocation, useNavigate, useResolvedPath, useRoute, useRouter } from "./routing";
6
- import { joinPaths, normalizePath } from "./utils";
6
+ import { joinPaths, normalizePath, createMemoObject } from "./utils";
7
7
  export const Router = (props) => {
8
8
  const { source, url, base, data, out } = props;
9
9
  const integration = source || (isServer ? staticIntegration({ value: url || "" }) : pathIntegration());
@@ -16,6 +16,14 @@ export const Routes = (props) => {
16
16
  const routeDefs = children(() => props.children);
17
17
  const branches = createMemo(() => createBranches(routeDefs(), joinPaths(parentRoute.pattern, props.base || ""), Outlet));
18
18
  const matches = createMemo(() => getRouteMatches(branches(), router.location.pathname));
19
+ const params = createMemoObject(() => {
20
+ const m = matches();
21
+ const params = {};
22
+ for (let i = 0; i < m.length; i++) {
23
+ Object.assign(params, m[i].params);
24
+ }
25
+ return params;
26
+ });
19
27
  if (router.out) {
20
28
  router.out.matches.push(matches().map(({ route, path, params }) => ({
21
29
  originalPath: route.originalPath,
@@ -42,7 +50,7 @@ export const Routes = (props) => {
42
50
  }
43
51
  createRoot(dispose => {
44
52
  disposers[i] = dispose;
45
- next[i] = createRouteContext(router, next[i - 1] || parentRoute, () => routeStates()[i + 1], () => matches()[i]);
53
+ next[i] = createRouteContext(router, next[i - 1] || parentRoute, () => routeStates()[i + 1], () => matches()[i], params);
46
54
  });
47
55
  }
48
56
  }
@@ -76,7 +84,14 @@ export const Outlet = () => {
76
84
  };
77
85
  export function A(props) {
78
86
  props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
79
- const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
87
+ const [, rest] = splitProps(props, [
88
+ "href",
89
+ "state",
90
+ "class",
91
+ "activeClass",
92
+ "inactiveClass",
93
+ "end"
94
+ ]);
80
95
  const to = useResolvedPath(() => props.href);
81
96
  const href = useHref(to);
82
97
  const location = useLocation();
package/dist/index.js CHANGED
@@ -148,9 +148,9 @@ function createBeforeLeave() {
148
148
  }
149
149
 
150
150
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
151
- const trimPathRegex = /^\/+|\/+$/g;
151
+ const trimPathRegex = /^\/+|(\/)\/+$/g;
152
152
  function normalizePath(path, omitSlash = false) {
153
- const s = path.replace(trimPathRegex, "");
153
+ const s = path.replace(trimPathRegex, "$1");
154
154
  return s ? omitSlash || /^[?#]/.test(s) ? s : "/" + s : "";
155
155
  }
156
156
  function resolvePath(base, path, from) {
@@ -185,7 +185,7 @@ function extractSearchParams(url) {
185
185
  });
186
186
  return params;
187
187
  }
188
- function createMatcher(path, partial) {
188
+ function createMatcher(path, partial, matchFilters) {
189
189
  const [pattern, splat] = path.split("/*", 2);
190
190
  const segments = pattern.split("/").filter(Boolean);
191
191
  const len = segments.length;
@@ -199,24 +199,47 @@ function createMatcher(path, partial) {
199
199
  path: len ? "" : "/",
200
200
  params: {}
201
201
  };
202
+ const matchFilter = s => matchFilters === undefined ? undefined : matchFilters[s];
202
203
  for (let i = 0; i < len; i++) {
203
204
  const segment = segments[i];
204
205
  const locSegment = locSegments[i];
205
- if (segment[0] === ":") {
206
- match.params[segment.slice(1)] = locSegment;
207
- } else if (segment.localeCompare(locSegment, undefined, {
208
- sensitivity: "base"
209
- }) !== 0) {
206
+ const dynamic = segment[0] === ":";
207
+ const key = dynamic ? segment.slice(1) : segment;
208
+ if (dynamic && matchSegment(locSegment, matchFilter(key))) {
209
+ match.params[key] = locSegment;
210
+ } else if (dynamic || !matchSegment(locSegment, segment)) {
210
211
  return null;
211
212
  }
212
213
  match.path += `/${locSegment}`;
213
214
  }
214
215
  if (splat) {
215
- match.params[splat] = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
216
+ const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
217
+ if (matchSegment(remainder, matchFilter(splat))) {
218
+ match.params[splat] = remainder;
219
+ } else {
220
+ return null;
221
+ }
216
222
  }
217
223
  return match;
218
224
  };
219
225
  }
226
+ function matchSegment(input, filter) {
227
+ const isEqual = s => s.localeCompare(input, undefined, {
228
+ sensitivity: "base"
229
+ }) === 0;
230
+ if (filter === undefined) {
231
+ return true;
232
+ } else if (typeof filter === "string") {
233
+ return isEqual(filter);
234
+ } else if (typeof filter === "function") {
235
+ return filter(input);
236
+ } else if (Array.isArray(filter)) {
237
+ return filter.some(isEqual);
238
+ } else if (filter instanceof RegExp) {
239
+ return filter.test(input);
240
+ }
241
+ return false;
242
+ }
220
243
  function scoreRoute(route) {
221
244
  const [pattern, splat] = route.pattern.split("/*", 2);
222
245
  const segments = pattern.split("/").filter(Boolean);
@@ -294,9 +317,9 @@ const useHref = to => {
294
317
  const useNavigate = () => useRouter().navigatorFactory();
295
318
  const useLocation = () => useRouter().location;
296
319
  const useIsRouting = () => useRouter().isRouting;
297
- const useMatch = path => {
320
+ const useMatch = (path, matchFilters) => {
298
321
  const location = useLocation();
299
- const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path)));
322
+ const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
300
323
  return createMemo(() => {
301
324
  for (const matcher of matchers()) {
302
325
  const match = matcher(location.pathname);
@@ -353,7 +376,7 @@ function createRoutes(routeDef, base = "", fallback) {
353
376
  ...shared,
354
377
  originalPath,
355
378
  pattern,
356
- matcher: createMatcher(pattern, !isLeaf)
379
+ matcher: createMatcher(pattern, !isLeaf, routeDef.matchFilters)
357
380
  });
358
381
  }
359
382
  return acc;
@@ -642,7 +665,7 @@ function createRouterContext(integration, base = "", data, out) {
642
665
  beforeLeave
643
666
  };
644
667
  }
645
- function createRouteContext(router, parent, child, match) {
668
+ function createRouteContext(router, parent, child, match, params) {
646
669
  const {
647
670
  base,
648
671
  location,
@@ -655,7 +678,6 @@ function createRouteContext(router, parent, child, match) {
655
678
  data
656
679
  } = match().route;
657
680
  const path = createMemo(() => match().path);
658
- const params = createMemoObject(() => match().params);
659
681
  preload && preload();
660
682
  const route = {
661
683
  parent,
@@ -713,6 +735,14 @@ const Routes = props => {
713
735
  const routeDefs = children(() => props.children);
714
736
  const branches = createMemo(() => createBranches(routeDefs(), joinPaths(parentRoute.pattern, props.base || ""), Outlet));
715
737
  const matches = createMemo(() => getRouteMatches(branches(), router.location.pathname));
738
+ const params = createMemoObject(() => {
739
+ const m = matches();
740
+ const params = {};
741
+ for (let i = 0; i < m.length; i++) {
742
+ Object.assign(params, m[i].params);
743
+ }
744
+ return params;
745
+ });
716
746
  if (router.out) {
717
747
  router.out.matches.push(matches().map(({
718
748
  route,
@@ -742,7 +772,7 @@ const Routes = props => {
742
772
  }
743
773
  createRoot(dispose => {
744
774
  disposers[i] = dispose;
745
- next[i] = createRouteContext(router, next[i - 1] || parentRoute, () => routeStates()[i + 1], () => matches()[i]);
775
+ next[i] = createRouteContext(router, next[i - 1] || parentRoute, () => routeStates()[i + 1], () => matches()[i], params);
746
776
  });
747
777
  }
748
778
  }
package/dist/routing.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Component, Accessor } from "solid-js";
2
- import type { BeforeLeaveEventArgs, Branch, Location, LocationChangeSignal, NavigateOptions, Navigator, Params, Route, RouteContext, RouteDataFunc, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SetParams } from "./types";
2
+ import type { BeforeLeaveEventArgs, Branch, Location, LocationChangeSignal, MatchFilters, NavigateOptions, Navigator, Params, Route, RouteContext, RouteDataFunc, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SetParams } from "./types";
3
3
  export declare const RouterContextObj: import("solid-js").Context<RouterContext | undefined>;
4
4
  export declare const RouteContextObj: import("solid-js").Context<RouteContext | undefined>;
5
5
  export declare const useRouter: () => RouterContext;
@@ -9,7 +9,7 @@ export declare const useHref: (to: () => string | undefined) => Accessor<string
9
9
  export declare const useNavigate: () => Navigator;
10
10
  export declare const useLocation: <S = unknown>() => Location<S>;
11
11
  export declare const useIsRouting: () => () => boolean;
12
- export declare const useMatch: (path: () => string) => Accessor<import("./types").PathMatch | undefined>;
12
+ export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S> | undefined) => Accessor<import("./types").PathMatch | undefined>;
13
13
  export declare const useParams: <T extends Params>() => T;
14
14
  type MaybeReturnType<T> = T extends (...args: any) => infer R ? R : T;
15
15
  export declare const useRouteData: <T>() => MaybeReturnType<T>;
@@ -21,5 +21,5 @@ export declare function createBranches(routeDef: RouteDefinition | RouteDefiniti
21
21
  export declare function getRouteMatches(branches: Branch[], location: string): RouteMatch[];
22
22
  export declare function createLocation(path: Accessor<string>, state: Accessor<any>): Location;
23
23
  export declare function createRouterContext(integration?: RouterIntegration | LocationChangeSignal, base?: string, data?: RouteDataFunc, out?: object): RouterContext;
24
- export declare function createRouteContext(router: RouterContext, parent: RouteContext, child: () => RouteContext, match: () => RouteMatch): RouteContext;
24
+ export declare function createRouteContext(router: RouterContext, parent: RouteContext, child: () => RouteContext, match: () => RouteMatch, params: Params): RouteContext;
25
25
  export {};
package/dist/routing.js CHANGED
@@ -23,9 +23,9 @@ export const useHref = (to) => {
23
23
  export const useNavigate = () => useRouter().navigatorFactory();
24
24
  export const useLocation = () => useRouter().location;
25
25
  export const useIsRouting = () => useRouter().isRouting;
26
- export const useMatch = (path) => {
26
+ export const useMatch = (path, matchFilters) => {
27
27
  const location = useLocation();
28
- const matchers = createMemo(() => expandOptionals(path()).map((path) => createMatcher(path)));
28
+ const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
29
29
  return createMemo(() => {
30
30
  for (const matcher of matchers()) {
31
31
  const match = matcher(location.pathname);
@@ -41,12 +41,20 @@ export const useSearchParams = () => {
41
41
  const navigate = useNavigate();
42
42
  const setSearchParams = (params, options) => {
43
43
  const searchString = untrack(() => mergeSearchString(location.search, params));
44
- navigate(location.pathname + searchString + location.hash, { scroll: false, resolve: false, ...options });
44
+ navigate(location.pathname + searchString + location.hash, {
45
+ scroll: false,
46
+ resolve: false,
47
+ ...options
48
+ });
45
49
  };
46
50
  return [location.query, setSearchParams];
47
51
  };
48
52
  export const useBeforeLeave = (listener) => {
49
- const s = useRouter().beforeLeave.subscribe({ listener, location: useLocation(), navigate: useNavigate() });
53
+ const s = useRouter().beforeLeave.subscribe({
54
+ listener,
55
+ location: useLocation(),
56
+ navigate: useNavigate()
57
+ });
50
58
  onCleanup(s);
51
59
  };
52
60
  export function createRoutes(routeDef, base = "", fallback) {
@@ -75,7 +83,7 @@ export function createRoutes(routeDef, base = "", fallback) {
75
83
  ...shared,
76
84
  originalPath,
77
85
  pattern,
78
- matcher: createMatcher(pattern, !isLeaf)
86
+ matcher: createMatcher(pattern, !isLeaf, routeDef.matchFilters)
79
87
  });
80
88
  }
81
89
  return acc;
@@ -363,11 +371,10 @@ export function createRouterContext(integration, base = "", data, out) {
363
371
  beforeLeave
364
372
  };
365
373
  }
366
- export function createRouteContext(router, parent, child, match) {
374
+ export function createRouteContext(router, parent, child, match, params) {
367
375
  const { base, location, navigatorFactory } = router;
368
376
  const { pattern, element: outlet, preload, data } = match().route;
369
377
  const path = createMemo(() => match().path);
370
- const params = createMemoObject(() => match().params);
371
378
  preload && preload();
372
379
  const route = {
373
380
  parent,
package/dist/types.d.ts CHANGED
@@ -40,8 +40,9 @@ export interface RouteDataFuncArgs<T = unknown> {
40
40
  navigate: Navigator;
41
41
  }
42
42
  export type RouteDataFunc<T = unknown, R = unknown> = (args: RouteDataFuncArgs<T>) => R;
43
- export type RouteDefinition = {
44
- path: string | string[];
43
+ export type RouteDefinition<S extends string | string[] = any> = {
44
+ path: S;
45
+ matchFilters?: MatchFilters<S>;
45
46
  data?: RouteDataFunc;
46
47
  children?: RouteDefinition | RouteDefinition[];
47
48
  } & ({
@@ -52,6 +53,11 @@ export type RouteDefinition = {
52
53
  element?: JSX.Element;
53
54
  preload?: () => void;
54
55
  });
56
+ export type MatchFilter = string[] | RegExp | ((s: string) => boolean);
57
+ export type PathParams<P extends string | readonly string[]> = P extends `${infer Head}/${infer Tail}` ? [...PathParams<Head>, ...PathParams<Tail>] : P extends `:${infer S}?` ? [S] : P extends `:${infer S}` ? [S] : P extends `*${infer S}` ? [S] : [];
58
+ export type MatchFilters<P extends string | readonly string[] = any> = P extends string ? {
59
+ [K in PathParams<P>[number]]?: MatchFilter;
60
+ } : Record<string, MatchFilter>;
55
61
  export interface PathMatch {
56
62
  params: Params;
57
63
  path: string;
@@ -73,6 +79,7 @@ export interface Route {
73
79
  preload?: () => void;
74
80
  data?: RouteDataFunc;
75
81
  matcher: (location: string) => PathMatch | null;
82
+ matchFilters?: MatchFilters;
76
83
  }
77
84
  export interface Branch {
78
85
  routes: Route[];
package/dist/utils.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import type { Params, PathMatch, Route, SetParams } from "./types";
1
+ import type { MatchFilters, Params, PathMatch, Route, SetParams } from "./types";
2
2
  export declare function normalizePath(path: string, omitSlash?: boolean): string;
3
3
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
4
4
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
5
5
  export declare function joinPaths(from: string, to: string): string;
6
6
  export declare function extractSearchParams(url: URL): Params;
7
- export declare function createMatcher(path: string, partial?: boolean): (location: string) => PathMatch | null;
7
+ export declare function createMatcher<S extends string>(path: S, partial?: boolean, matchFilters?: MatchFilters<S>): (location: string) => PathMatch | null;
8
8
  export declare function scoreRoute(route: Route): number;
9
9
  export declare function createMemoObject<T extends Record<string | symbol, unknown>>(fn: () => T): T;
10
10
  export declare function mergeSearchString(search: string, params: SetParams): string;
package/dist/utils.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { createMemo, getOwner, runWithOwner } from "solid-js";
2
2
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
3
- const trimPathRegex = /^\/+|\/+$/g;
3
+ const trimPathRegex = /^\/+|(\/)\/+$/g;
4
4
  export function normalizePath(path, omitSlash = false) {
5
- const s = path.replace(trimPathRegex, "");
5
+ const s = path.replace(trimPathRegex, "$1");
6
6
  return s ? (omitSlash || /^[?#]/.test(s) ? s : "/" + s) : "";
7
7
  }
8
8
  export function resolvePath(base, path, from) {
@@ -39,7 +39,7 @@ export function extractSearchParams(url) {
39
39
  });
40
40
  return params;
41
41
  }
42
- export function createMatcher(path, partial) {
42
+ export function createMatcher(path, partial, matchFilters) {
43
43
  const [pattern, splat] = path.split("/*", 2);
44
44
  const segments = pattern.split("/").filter(Boolean);
45
45
  const len = segments.length;
@@ -53,23 +53,51 @@ export function createMatcher(path, partial) {
53
53
  path: len ? "" : "/",
54
54
  params: {}
55
55
  };
56
+ const matchFilter = (s) => matchFilters === undefined ? undefined : matchFilters[s];
56
57
  for (let i = 0; i < len; i++) {
57
58
  const segment = segments[i];
58
59
  const locSegment = locSegments[i];
59
- if (segment[0] === ":") {
60
- match.params[segment.slice(1)] = locSegment;
60
+ const dynamic = segment[0] === ":";
61
+ const key = dynamic ? segment.slice(1) : segment;
62
+ if (dynamic && matchSegment(locSegment, matchFilter(key))) {
63
+ match.params[key] = locSegment;
61
64
  }
62
- else if (segment.localeCompare(locSegment, undefined, { sensitivity: "base" }) !== 0) {
65
+ else if (dynamic || !matchSegment(locSegment, segment)) {
63
66
  return null;
64
67
  }
65
68
  match.path += `/${locSegment}`;
66
69
  }
67
70
  if (splat) {
68
- match.params[splat] = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
71
+ const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
72
+ if (matchSegment(remainder, matchFilter(splat))) {
73
+ match.params[splat] = remainder;
74
+ }
75
+ else {
76
+ return null;
77
+ }
69
78
  }
70
79
  return match;
71
80
  };
72
81
  }
82
+ function matchSegment(input, filter) {
83
+ const isEqual = (s) => s.localeCompare(input, undefined, { sensitivity: "base" }) === 0;
84
+ if (filter === undefined) {
85
+ return true;
86
+ }
87
+ else if (typeof filter === "string") {
88
+ return isEqual(filter);
89
+ }
90
+ else if (typeof filter === "function") {
91
+ return filter(input);
92
+ }
93
+ else if (Array.isArray(filter)) {
94
+ return filter.some(isEqual);
95
+ }
96
+ else if (filter instanceof RegExp) {
97
+ return filter.test(input);
98
+ }
99
+ return false;
100
+ }
73
101
  export function scoreRoute(route) {
74
102
  const [pattern, splat] = route.pattern.split("/*", 2);
75
103
  const segments = pattern.split("/").filter(Boolean);
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.6.0",
9
+ "version": "0.7.1",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",