Skip to content

Commit 1bf46d1

Browse files
jchatardJorrenH
andauthored
Segment validators (#227)
* Add Segment Validators to allow more complex path matching. This allows to add validator functions at the segment level. * Remove the ~ prefix, : colon is already enough. * Apply matchFilter naming and extend matching functionality * Adding type inference for Route array paths * Update test on route specs. Co-authored-by: Jorren <jorrenhendriks@gmail.com>
1 parent 5438d5d commit 1bf46d1

8 files changed

Lines changed: 380 additions & 82 deletions

File tree

README.md

Lines changed: 89 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
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.
88

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.
1010

1111
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/)).
1212

@@ -16,8 +16,8 @@ It supports all of Solid's SSR methods and has Solid's transitions baked in, so
1616
- [Create Links to Your Routes](#create-links-to-your-routes)
1717
- [Dynamic Routes](#dynamic-routes)
1818
- [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)
2121
- [Config Based Routing](#config-based-routing)
2222
- [Router Primitives](#router-primitives)
2323
- [useParams](#useparams)
@@ -63,17 +63,16 @@ Solid Router allows you to configure your routes using JSX:
6363

6464
1. Use the `Routes` component to specify where the routes should appear in your app.
6565

66-
6766
```jsx
6867
import { Routes, Route } from "@solidjs/router"
6968

7069
export default function App() {
7170
return <>
72-
<h1>My Site with Lots of Pages</h1>
71+
<h1>My Site with Lots of Pages</h1>
7372
<Routes>
7473

7574
</Routes>
76-
</>
75+
</>
7776
}
7877
```
7978

@@ -87,13 +86,13 @@ import Users from "./pages/Users"
8786

8887
export default function App() {
8988
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+
</>
9796
}
9897
```
9998

@@ -109,13 +108,13 @@ const Home = lazy(() => import("./pages/Home"));
109108

110109
export default function App() {
111110
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+
</>
119118
}
120119
```
121120

@@ -131,17 +130,17 @@ const Home = lazy(() => import("./pages/Home"));
131130

132131
export default function App() {
133132
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+
</>
145144
}
146145
```
147146

@@ -163,7 +162,7 @@ Solid Router provides a `Navigate` component that works similarly to `A`, but it
163162

164163
```jsx
165164
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().
167166
//You can use those to dynamically determine a path to navigate to
168167
return "/some-path";
169168
}
@@ -174,7 +173,7 @@ function getPath ({navigate, location}) {
174173

175174
## Dynamic Routes
176175

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.
178177

179178
```jsx
180179
import { lazy } from "solid-js";
@@ -185,21 +184,63 @@ const Home = lazy(() => import("./pages/Home"));
185184

186185
export default function App() {
187186
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+
</>
196195
}
197196
```
198197

199198
The colon indicates that `id` can be any string, and as long as the URL fits that pattern, the `User` component will show.
200199

201200
You can then access that `id` from within a route component with `useParams`:
202201

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+
---
203244

204245
```jsx
205246
//async fetching function
@@ -250,12 +291,10 @@ Routes also support defining multiple paths using an array. This allows a route
250291
<Route path={["login", "register"]} component={Login}/>
251292
```
252293

253-
254294
## Data Functions
255295
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.
256296

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.
259298

260299
```js
261300
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
299338
```js
300339
import { lazy } from "solid-js";
301340
import { Route } from "@solidjs/router";
302-
import { fetchUser } ...
341+
import { fetchUser } ...
303342
import UserData from "./pages/users/[id].data.js";
304343
const User = lazy(() => import("/pages/users/[id].js"));
305344

@@ -341,15 +380,14 @@ Only leaf Route nodes (innermost `Route` components) are given a route. If you w
341380

342381
You can also take advantage of nesting by adding a parent element with an `<Outlet/>`.
343382
```jsx
344-
345383
import { Outlet } from "@solidjs/router";
346384

347385
function PageWrapper () {
348386
return <div>
349-
<h1> We love our users! </h1>
387+
<h1> We love our users! </h1>
350388
<Outlet/>
351-
<A href="/">Back Home</A>
352-
</div>
389+
<A href="/">Back Home</A>
390+
</div>
353391
}
354392

355393
<Route path="/users" component={PageWrapper}>
@@ -369,7 +407,7 @@ You can nest indefinitely - just remember that only leaf nodes will become their
369407
</Route>
370408
```
371409

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.
373411

374412
## Hash Mode Router
375413

@@ -560,11 +598,9 @@ useBeforeLeave((e: BeforeLeaveEventArgs) => {
560598
setTimeout(() => {
561599
if (window.confirm("Discard unsaved changes - are you sure?")) {
562600
// user wants to proceed anyway so retry with force=true
563-
e.retry(true);
601+
e.retry(true);
564602
}
565603
}, 100);
566604
}
567605
});
568606
```
569-
570-

src/components.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import type {
2222
Location,
2323
LocationChangeSignal,
24+
MatchFilters,
2425
Navigator,
2526
RouteContext,
2627
RouteDataFunc,
@@ -151,10 +152,11 @@ export const useRoutes = (routes: RouteDefinition | RouteDefinition[], base?: st
151152
return () => <Routes base={base}>{routes as any}</Routes>;
152153
};
153154

154-
export type RouteProps = {
155-
path: string | string[];
155+
export type RouteProps<S extends string> = {
156+
path: S | S[];
156157
children?: JSX.Element;
157158
data?: RouteDataFunc;
159+
matchFilters?: MatchFilters<S>;
158160
} & (
159161
| {
160162
element?: never;
@@ -167,7 +169,7 @@ export type RouteProps = {
167169
}
168170
);
169171

170-
export const Route = (props: RouteProps) => {
172+
export const Route = <S extends string>(props: RouteProps<S>) => {
171173
const childRoutes = children(() => props.children);
172174
return mergeProps(props, {
173175
get children() {
@@ -200,7 +202,14 @@ export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorEle
200202
}
201203
export function A(props: AnchorProps) {
202204
props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
203-
const [, rest] = splitProps(props, ["href", "state", "class", "activeClass", "inactiveClass", "end"]);
205+
const [, rest] = splitProps(props, [
206+
"href",
207+
"state",
208+
"class",
209+
"activeClass",
210+
"inactiveClass",
211+
"end"
212+
]);
204213
const to = useResolvedPath(() => props.href);
205214
const href = useHref(to);
206215
const location = useLocation();

0 commit comments

Comments
 (0)