Skip to content

Commit 85623c9

Browse files
authored
Merge pull request #6 from notnotsamuel/Nested-routing-refactor-&-Navigation-Guards
Nested routing refactor & navigation guards
2 parents c369692 + b560677 commit 85623c9

12 files changed

+561
-71
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
node_modules
22
dist
33
*.log
4-
dummy
54

65
.DS_Store

README.md

Lines changed: 180 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,35 @@ npm i svelte-tiny-router
88
```
99

1010
## Use
11+
12+
Here's a basic example demonstrating simple routes:
13+
14+
```svelte
15+
<!-- App.svelte -->
16+
<script>
17+
import { Router, Route } from 'svelte-tiny-router';
18+
import Home from './Home.svelte';
19+
import About from './About.svelte';
20+
import User from './User.svelte';
21+
</script>
22+
23+
<Router>
24+
<!-- Exact match for home page -->
25+
<Route path="/" component={Home} />
26+
27+
<!-- Static route -->
28+
<Route path="/about" component={About} />
29+
30+
<!-- Dynamic route: "/user/123" will match and pass { id: "123" } as a prop -->
31+
<Route path="/user/:id" component={User} />
32+
33+
<!-- Fallback route: no "path" prop means it always matches (e.g. for a 404 page) -->
34+
<Route>
35+
<p>Page not found.</p>
36+
</Route>
37+
</Router>
38+
```
39+
1140
### route
1241
```svelte
1342
<!-- App.svelte -->
@@ -39,14 +68,30 @@ npm i svelte-tiny-router
3968
```svelte
4069
<!-- SomeComponent.svelte -->
4170
<script>
42-
import { navigate } from 'svelte-tiny-router';
71+
import { useTinyRouter } from 'svelte-tiny-router';
72+
const router = useTinyRouter();
4373
4474
function goToAbout() {
45-
navigate('/about');
75+
router.navigate('/about'); // Use router.navigate
76+
}
77+
78+
function goToUser(id) {
79+
router.navigate(`/user/${id}`);
80+
}
81+
82+
function replaceWithHome() {
83+
router.navigate('/', { replace: true }); // Replace current history entry
84+
}
85+
86+
function navigateWithQuery() {
87+
router.navigate('/search?q=svelte&category=router'); // Navigate with query string
4688
}
4789
</script>
4890
4991
<button on:click={goToAbout}>Go to About Page</button>
92+
<button on:click={() => goToUser(123)}>Go to User 123</button>
93+
<button on:click={replaceWithHome}>Replace with Home</button>
94+
<button on:click={navigateWithQuery}>Search</button>
5095
```
5196

5297
### get then remove query strings
@@ -56,10 +101,143 @@ npm i svelte-tiny-router
56101
import { useTinyRouter } from 'svelte-tiny-router';
57102
const router = useTinyRouter();
58103
104+
// Access the entire query object
105+
console.log("Current query:", router.query);
106+
59107
// Check if the "foo" query parameter exists (i.e /myroute?foo=bar) and log it
60108
if (router.hasQueryParam('foo')) {
61109
console.log("Value of foo:", router.getQueryParam('foo'));
62110
router.removeQueryParams(["foo"]);
63111
}
112+
113+
// Get a specific query parameter
114+
const searchTerm = router.getQueryParam('q');
115+
console.log("Search term:", searchTerm);
64116
</script>
65117
```
118+
119+
## Navigation Guards (`beforeEach`)
120+
121+
You can define navigation guards using the `beforeEach` prop on the `<Router>` component. These guards are functions that are executed before each navigation. They can be used to cancel navigation, redirect to a different route, or perform asynchronous tasks like authentication checks.
122+
123+
```svelte
124+
<!-- App.svelte (with navigation guards) -->
125+
<script>
126+
import { Router, Route } from 'svelte-tiny-router';
127+
import Home from './Home.svelte';
128+
import AdminDashboard from './AdminDashboard.svelte';
129+
import Login from './Login.svelte';
130+
131+
// Example authentication check function
132+
function isAuthenticated() {
133+
// Replace with your actual auth logic (e.g., check token in localStorage)
134+
return localStorage.getItem('authToken') !== null;
135+
}
136+
137+
// Define navigation guards
138+
const authGuard = async ({ to, from, next }) => {
139+
console.log('[authGuard] Navigating from:', from?.path, 'to:', to.path, 'Query:', to.query);
140+
if (to.path.startsWith('/admin') && !isAuthenticated()) {
141+
console.log('Authentication required for admin route, redirecting to login.');
142+
// Redirect to login page, replacing the current history entry
143+
next({ path: '/login', replace: true });
144+
} else {
145+
// Continue navigation
146+
next();
147+
}
148+
};
149+
150+
const loggingGuard = ({ to, from, next }) => {
151+
console.log('[LOG] Navigation attempt:', from?.path || 'N/A', '->', to.path, 'Query:', to.query);
152+
next(); // Always call next() to proceed
153+
};
154+
155+
const myGuards = [loggingGuard, authGuard]; // Guards are executed in order
156+
</script>
157+
158+
<Router beforeEach={myGuards}>
159+
<Route path="/" component={Home} />
160+
<Route path="/admin" component={AdminDashboard} />
161+
<Route path="/login" component={Login} />
162+
<Route>
163+
<p>Page not found.</p>
164+
</Route>
165+
</Router>
166+
```
167+
168+
A navigation guard function receives an object with the following properties:
169+
- `to`: An object representing the target route (`{ path: string, params: Record<string, string>, query: Record<string, string> }`).
170+
- `from`: An object representing the current route, or `null` if this is the initial navigation (`{ path: string, params: Record<string, string>, query: Record<string, string> } | null`).
171+
- `next`: A function that must be called to resolve the hook.
172+
- `next()`: Proceed to the next hook in the pipeline, or to the navigation if no more hooks are left.
173+
- `next(false)`: Cancel the current navigation.
174+
- `next('/path')` or `next({ path: '/path', replace: true })`: Redirect to a different location. The current navigation is cancelled, and a new one is initiated.
175+
176+
## Nested Routing
177+
178+
The library supports nested routing, particularly useful with wildcard routes (`/*`). When a wildcard route matches, it automatically sets up a `NestedRouterProvider` context for its children `<Route>` components. These children routes then match paths relative to the parent wildcard's matched segment.
179+
180+
For example, with a structure like:
181+
```svelte
182+
<Router>
183+
<Route path="/app/*">
184+
<Route path="/" component={AppHome} /> {/* Matches /app */}
185+
<Route path="/settings" component={AppSettings} /> {/* Matches /app/settings */}
186+
</Route>
187+
</Router>
188+
```
189+
Navigating to `/app/settings` will first match the `/app/*` route. The `NestedRouterProvider` within `/app/*` then makes `/settings` match relative to `/app`.
190+
191+
Alternatively, you can render a separate component that contains its own `<Router>` instance for nested routes. This component will receive the matched parameters from the parent route.
192+
193+
```svelte
194+
<!-- App.svelte -->
195+
<script>
196+
import { Router, Route } from 'svelte-tiny-router';
197+
import Home from './Home.svelte';
198+
import About from './About.svelte';
199+
import User from './User.svelte';
200+
import DashboardRouter from './DashboardRouter.svelte'; // Component containing nested routes
201+
</script>
202+
203+
<Router>
204+
<Route path="/" component={Home} />
205+
<Route path="/about" component={About} />
206+
<Route path="/user/:id" component={User} />
207+
208+
<!-- Wildcard route rendering a component that contains a nested router -->
209+
<Route path="/dashboard/*" component={DashboardRouter} />
210+
211+
<Route>
212+
<p>Page not found.</p>
213+
</Route>
214+
</Router>
215+
```
216+
217+
```svelte
218+
<!-- DashboardRouter.svelte -->
219+
<script>
220+
import { Router, Route } from 'svelte-tiny-router';
221+
import DashboardHome from './DashboardHome.svelte';
222+
import Profile from './Profile.svelte';
223+
import Settings from './Settings.svelte';
224+
225+
// This component receives params from the parent route if any were captured
226+
// let { paramFromParent } = $props(); // Example if /dashboard/:param/* was used
227+
</script>
228+
229+
<!-- This Router instance handles routes relative to the parent's matched path (/dashboard) -->
230+
<Router>
231+
<Route path="/" component={DashboardHome} /> {/* Matches /dashboard */}
232+
<Route path="/profile" component={Profile} /> {/* Matches /dashboard/profile */}
233+
<Route path="/settings" component={Settings} /> {/* Matches /dashboard/settings */}
234+
<!-- Nested fallback for /dashboard/* -->
235+
<Route>
236+
<p>Dashboard page not found.</p>
237+
</Route>
238+
</Router>
239+
```
240+
241+
## Type Definitions
242+
243+
This library now includes comprehensive TypeScript definitions, providing improved type checking and autocompletion for users of TypeScript or JavaScript with JSDoc. Key types include `RouterContext`, `RouteInfo`, `NavigationGuard`, and `NextFunction`.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"name": "svelte-tiny-router",
3-
"version": "1.0.4",
3+
"version": "1.0.5",
44
"author": "notnotsamuel",
55
"description": "A simple and efficient declarative routing library for Svelte 5 built with runes.",
66
"type": "module",
77
"main": "dist/svelte-tiny-router.umd.js",
88
"module": "dist/svelte-tiny-router.es.js",
9+
"types": "src/lib/index.d.ts",
910
"svelte": "src/lib/index.js",
1011
"files": [
1112
"dist",

src/lib/NestedRouterProvider.svelte

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script>
2+
import { setContext } from 'svelte';
3+
4+
let { parentCtx, matchedPath } = $props();
5+
6+
const matchedBaseSegments = matchedPath.slice(0, -2).split('/').filter(Boolean); // e.g., ['dashboard'] from '/dashboard/*'
7+
8+
// Calculate the new base path for children of the wildcard route.
9+
// It combines the parent's base with the static part of the matched wildcard path.
10+
let newBaseForChildren = parentCtx.base.replace(/\/$/, ''); // Ensure no trailing slash from parent base
11+
if (matchedBaseSegments.length > 0) {
12+
newBaseForChildren += '/' + matchedBaseSegments.join('/');
13+
}
14+
15+
// Normalize newBaseForChildren: ensure it starts with '/', does NOT end with '/', unless it's the root '/'
16+
if (newBaseForChildren !== '/') {
17+
newBaseForChildren = ('/' + newBaseForChildren.replace(/^\/+|\/+$/g, '')).replace(/\/\//g, '/');
18+
if (newBaseForChildren === '') newBaseForChildren = '/'; // Handle case where it might become empty
19+
}
20+
21+
22+
// Calculate the path for the children's context. This is the remaining part of the parent's *relative* path
23+
// after the part matched by the wildcard's static segments.
24+
const parentRelativePathSegments = parentCtx.path.split('/').filter(Boolean);
25+
const remainingChildPath = '/' + parentRelativePathSegments.slice(matchedBaseSegments.length).join('/');
26+
27+
const contextForSlot = {
28+
...parentCtx, // Inherit most properties (navigate, query utils, fullPath, etc.)
29+
path: remainingChildPath, // Path relative to the newBaseForChildren
30+
base: newBaseForChildren, // The new base path for this nested scope
31+
routes: [], // Nested router instances get their own routes array
32+
33+
// These are signals for a nested Router to know it's under a wildcard
34+
isWildcardChildContext: true,
35+
// fromPath will be inherited or updated by the nested Router itself.
36+
};
37+
38+
setContext('router', contextForSlot);
39+
</script>
40+
41+
<slot />

0 commit comments

Comments
 (0)