Skip to content

Commit 53790e2

Browse files
webJoseCopilot
andauthored
feat: Add ignoreForFallback property to Route (#43)
* feat: Add ignoreForFallback property to Route This also removes the when property. Fixes #20. * Remove logging from test file Co-authored-by: Copilot <[email protected]> * demo: Remove the use of Route.when This has restored functionality to the Fallback content in the demo. --------- Co-authored-by: Copilot <[email protected]>
1 parent 3d38fc2 commit 53790e2

File tree

7 files changed

+151
-98
lines changed

7 files changed

+151
-98
lines changed

demo/src/App.svelte

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
const timer = setTimeout(() => {
1616
showNavTooltip = true;
1717
}, 2000);
18-
18+
1919
// Hide tooltip after 10 seconds or when user interacts
2020
const hideTimer = setTimeout(() => {
2121
showNavTooltip = false;
2222
}, 12000);
23-
23+
2424
return () => {
2525
clearTimeout(timer);
2626
clearTimeout(hideTimer);
@@ -31,33 +31,35 @@
3131
<div class="app">
3232
<div class="d-flex flex-column h-100">
3333
<Router id="root">
34-
<Tooltip shown={showNavTooltip} placement="bottom">
35-
{#snippet reference(ref)}
36-
<NavBar {@attach ref} />
37-
{/snippet}
38-
Use these navigation links to test-drive the routing capabilities of @wjfe/n-savant.
39-
</Tooltip>
40-
<main class="d-flex flex-column flex-fill overflow-auto mt-3">
41-
<div class="container-fluid flex-fill d-flex flex-column">
42-
<div class="grid flex-fill">
43-
<Route key="home" path="/">
44-
<HomeView />
45-
</Route>
46-
<Route key="pathRouting" path="/path-routing/*">
47-
<PathRoutingView basePath="/path-routing" />
48-
</Route>
49-
<Route key="hashRouting" path="/hash-routing">
50-
<HashRoutingView basePath="/hash-routing" />
51-
</Route>
52-
<Fallback>
53-
<NotFound />
54-
</Fallback>
34+
{#snippet children(_, rs)}
35+
<Tooltip shown={showNavTooltip} placement="bottom">
36+
{#snippet reference(ref)}
37+
<NavBar {@attach ref} />
38+
{/snippet}
39+
Use these navigation links to test-drive the routing capabilities of @wjfe/n-savant.
40+
</Tooltip>
41+
<main class="d-flex flex-column flex-fill overflow-auto mt-3">
42+
<div class="container-fluid flex-fill d-flex flex-column">
43+
<div class="grid flex-fill">
44+
<Route key="home" path="/">
45+
<HomeView />
46+
</Route>
47+
<Route key="pathRouting" path="/path-routing/*">
48+
<PathRoutingView basePath="/path-routing" />
49+
</Route>
50+
<Route key="hashRouting" path="/hash-routing">
51+
<HashRoutingView basePath="/hash-routing" />
52+
</Route>
53+
<Fallback>
54+
<NotFound />
55+
</Fallback>
56+
</div>
5557
</div>
56-
</div>
57-
</main>
58-
<Route key="notHome" when={(rs) => !rs.home.match}>
59-
<RouterTrace />
60-
</Route>
58+
</main>
59+
{#if !rs.home.match}
60+
<RouterTrace />
61+
{/if}
62+
{/snippet}
6163
</Router>
6264
</div>
6365
</div>

demo/src/lib/NavBar.svelte

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import { routingMode } from './hash-routing';
55
import type { HTMLAttributes } from 'svelte/elements';
66
7-
let {
8-
...restProps
9-
}: HTMLAttributes<HTMLElement> = $props();
7+
let { ...restProps }: HTMLAttributes<HTMLElement> = $props();
108
119
const pathRoutingLinks = [
1210
{ text: 'Home', href: '/path-routing' },
@@ -62,32 +60,45 @@
6260
<div class="collapse navbar-collapse" id="navbarNav">
6361
<ul class="navbar-nav">
6462
<li class="nav-item">
65-
<Link class="nav-link" activeState={{ class: 'active', key: 'home' }} href="/" id="homeLink">Home</Link>
63+
<Link
64+
class="nav-link"
65+
activeState={{ class: 'active', key: 'home' }}
66+
href="/"
67+
id="homeLink">Home</Link
68+
>
6669
</li>
67-
<Route key="homeMenuPr" when={(rs) => !rs.pathRouting?.match}>
68-
<li class="nav-item">
69-
<Link
70-
class="nav-link"
71-
activeState={{ class: 'active', key: 'pathRouting' }}
72-
href="/path-routing"
73-
>
74-
Path Routing
75-
</Link>
76-
</li>
70+
<Route key="homeMenuPr">
71+
{#snippet children(rp, _, rs)}
72+
{#if !rs.pathRouting?.match}
73+
<li class="nav-item">
74+
<Link
75+
class="nav-link"
76+
activeState={{ class: 'active', key: 'pathRouting' }}
77+
href="/path-routing"
78+
>
79+
Path Routing
80+
</Link>
81+
</li>
82+
{/if}
83+
{/snippet}
7784
</Route>
7885
<Route key="pathRouting">
7986
<SubNav title="Path Routing" links={pathRoutingLinks} />
8087
</Route>
81-
<Route key="homeMenuHr" when={(rs) => !rs.hashRouting?.match}>
82-
<li class="nav-item">
83-
<Link
84-
class="nav-link"
85-
activeState={{ class: 'active', key: 'hashRouting' }}
86-
href="/hash-routing"
87-
>
88-
Hash Routing
89-
</Link>
90-
</li>
88+
<Route key="homeMenuHr">
89+
{#snippet children(rp, _, rs)}
90+
{#if !rs.hashRouting?.match}
91+
<li class="nav-item">
92+
<Link
93+
class="nav-link"
94+
activeState={{ class: 'active', key: 'hashRouting' }}
95+
href="/hash-routing"
96+
>
97+
Hash Routing
98+
</Link>
99+
</li>
100+
{/if}
101+
{/snippet}
91102
</Route>
92103
<Route key="hashRouting">
93104
<SubNav title="Hash Routing" links={hashRoutingLinks} />

src/lib/Route/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ they can be embedded anywhere down the hierarchy, including being children of ot
1313
| `key` | `string` | (none) | | Sets the route's unique key. |
1414
| `path` | `string \| RegExp` | (none) | | Sets the route's path pattern, or a regular expression used to test and match the browser's URL. |
1515
| `and` | `(params: Record<RouteParameters<T>, ParameterValue> \| undefined) => boolean` | `undefined` | | Sets a function for additional matching conditions. |
16-
| `when` | `(routeStatus: Record<string, RouteStatus>) => boolean` | `undefined` | | Sets a function for additional matching conditions. |
16+
| `ignoreForFallback` | `boolean` | `false` | | Controls whether the matching status of this route affects the visibility of fallback content. |
1717
| `caseSensitive` | `boolean` | `false` | | Sets whether the route's path pattern should be matched case-sensitively. |
1818
| `hash` | `boolean \| string` | `undefined` | | Sets the hash mode of the route. |
1919
| `params` | `Record<RouteParameters<T>, ParameterValue>` | `undefined` | Yes | Provides a way to obtain a route's parameters through property binding. |

src/lib/Route/Route.svelte

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,32 +71,11 @@
7171
*/
7272
and?: (params: Record<RouteParameters<T>, ParameterValue> | undefined) => boolean;
7373
/**
74-
* Sets a function for additional matching conditions.
75-
*
76-
* Use this one when you need to match based on the final status of all routes.
77-
* @param routeStatus The router's route status object.
78-
* @returns `true` if the route should match, or `false` otherwise.
74+
* Sets whether the route's match status should be ignored for fallback purposes.
7975
*
80-
* This is shorthand for:
81-
*
82-
* ```svelte
83-
* {#if when(router.routeStatus)}
84-
* <Route ...>...</Route>
85-
* {/if}
86-
* ```
87-
*
88-
*
89-
* In other words, use it to further condition rendering based on the final status of all routes.
90-
*
91-
* Example: Match only if the home route did not:
92-
*
93-
* ```svelte
94-
* <Route key="notHome" when={({ home }) => !home.match}>
95-
* <NotHome />
96-
* </Route>
97-
* ```
76+
* If `true`, the route will not be considered when determining fallback content visibility.
9877
*/
99-
when?: (routeStatus: Record<string, RouteStatus>) => boolean;
78+
ignoreForFallback?: boolean;
10079
/**
10180
* Sets whether the route's path pattern should be matched case-sensitively.
10281
*
@@ -146,7 +125,7 @@
146125
key,
147126
path,
148127
and,
149-
when,
128+
ignoreForFallback = false,
150129
caseSensitive = false,
151130
hash,
152131
params = $bindable(),
@@ -162,17 +141,17 @@
162141
163142
// Effect that updates the route object in the parent router.
164143
$effect.pre(() => {
165-
if (!path && !and && !when) {
144+
if (!path && !and) {
166145
return;
167146
}
168147
// svelte-ignore ownership_invalid_mutation
169148
untrack(() => router.routes)[key] =
170149
path instanceof RegExp
171-
? { regex: path, and, when }
150+
? { regex: path, and, ignoreForFallback }
172151
: {
173152
pattern: path,
174153
and,
175-
when,
154+
ignoreForFallback,
176155
caseSensitive
177156
};
178157
return () => {
@@ -186,6 +165,6 @@
186165
});
187166
</script>
188167

189-
{#if (router.routeStatus[key]?.match ?? true) && (untrack(() => router.routes)[key]?.when?.(router.routeStatus) ?? true)}
168+
{#if (router.routeStatus[key]?.match ?? true)}
190169
{@render children?.(params, router.state, router.routeStatus)}
191170
{/if}

src/lib/core/RouterEngine.svelte.test.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, test, expect, beforeAll, afterAll, vi } from "vitest";
1+
import { describe, test, expect, beforeAll, afterAll, vi, beforeEach } from "vitest";
22
import { routePatternsKey, RouterEngine } from "./RouterEngine.svelte.js";
33
import { init, type Hash, type RouteInfo } from "$lib/index.js";
44
import { registerRouter } from "./trace.svelte.js";
@@ -39,7 +39,7 @@ describe("RouterEngine", () => {
3939
});
4040
});
4141

42-
describe("RouterEngine", () => {
42+
describe("RouterEngine (default init)", () => {
4343
let _href: string;
4444
let cleanup: () => void;
4545
let interceptedState: any = null;
@@ -73,6 +73,9 @@ describe("RouterEngine", () => {
7373
replaceState: replaceStateMock
7474
};
7575
});
76+
beforeEach(() => {
77+
location.url.href = globalThis.window.location.href = "http://example.com";
78+
});
7679
afterAll(() => {
7780
cleanup();
7881
});
@@ -497,9 +500,72 @@ describe("RouterEngine", () => {
497500
});
498501
});
499502
});
503+
describe('noMatches', () => {
504+
test("Should be true whenever there are no routes registered.", () => {
505+
// Act.
506+
const router = new RouterEngine();
507+
508+
// Assert.
509+
expect(router.noMatches).toBe(true);
510+
});
511+
test("Should be true whenever there are no matching routes.", () => {
512+
// Act.
513+
const router = new RouterEngine();
514+
router.routes['route'] = {
515+
pattern: '/:one/:two?',
516+
caseSensitive: false,
517+
};
518+
519+
// Assert.
520+
expect(router.noMatches).toBe(true);
521+
});
522+
test.each([
523+
{
524+
text: "is",
525+
routeCount: 1,
526+
totalRoutes: 5
527+
},
528+
{
529+
text: "are",
530+
routeCount: 2,
531+
totalRoutes: 5
532+
},
533+
{
534+
text: "are",
535+
routeCount: 5,
536+
totalRoutes: 5
537+
},
538+
])("Should be false whenever there $text $routeCount matching route(s) out of $totalRoutes route(s).", ({ routeCount, totalRoutes }) => {
539+
// Act.
540+
const router = new RouterEngine();
541+
for (let i = 0; i < routeCount; i++) {
542+
router.routes[`route${i}`] = {
543+
and: () => i < routeCount
544+
};
545+
}
546+
547+
// Assert.
548+
expect(router.noMatches).toBe(false);
549+
});
550+
test.each([
551+
1, 2, 5
552+
])("Should be true whenever the %d matching route(s) are ignored for fallback.", (routeCount) => {
553+
// Act.
554+
const router = new RouterEngine();
555+
for (let i = 0; i < routeCount; i++) {
556+
router.routes[`route${i}`] = {
557+
and: () => true,
558+
ignoreForFallback: true
559+
};
560+
}
561+
562+
// Assert.
563+
expect(router.noMatches).toBe(true);
564+
});
565+
});
500566
});
501567

502-
describe("RouterEngine", () => {
568+
describe("RouterEngine (multi hash)", () => {
503569
let _href: string;
504570
let cleanup: () => void;
505571
let interceptedState: any = null;

src/lib/core/RouterEngine.svelte.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ export class RouterEngine {
127127
#routePatterns = $derived(Object.entries(this.routes).reduce((map, [key, route]) => {
128128
map.set(
129129
key, routeInfoIsRegexInfo(route) ?
130-
{ regex: route.regex, and: route.and } :
130+
{ regex: route.regex, and: route.and, ignoreForFallback: !!route.ignoreForFallback } :
131131
this.#parseRoutePattern(route)
132132
);
133133
return map;
134-
}, new Map<string, { regex?: RegExp; and?: AndUntyped; }>()));
134+
}, new Map<string, { regex?: RegExp; and?: AndUntyped; ignoreForFallback: boolean; }>()));
135135

136136
[routePatternsKey]() {
137137
return this.#routePatterns;
@@ -156,7 +156,7 @@ export class RouterEngine {
156156
}
157157
}
158158
const match = (!!matches || !pattern.regex) && (!pattern.and || pattern.and(routeParams));
159-
noMatches = noMatches && !match;
159+
noMatches = noMatches && (pattern.ignoreForFallback ? true : !match);
160160
routeStatus[routeKey] = {
161161
match,
162162
routeParams,
@@ -179,10 +179,11 @@ export class RouterEngine {
179179
* @param routeInfo Pattern route information to parse.
180180
* @returns An object with the regular expression and the optional predicate function.
181181
*/
182-
#parseRoutePattern(routeInfo: PatternRouteInfo): { regex?: RegExp; and?: AndUntyped; } {
182+
#parseRoutePattern(routeInfo: PatternRouteInfo): { regex?: RegExp; and?: AndUntyped; ignoreForFallback: boolean; } {
183183
if (!routeInfo.pattern) {
184184
return {
185185
and: routeInfo.and,
186+
ignoreForFallback: !!routeInfo.ignoreForFallback
186187
}
187188
}
188189
const fullPattern = joinPaths(this.basePath, routeInfo.pattern === '/' ? '' : routeInfo.pattern);
@@ -196,6 +197,7 @@ export class RouterEngine {
196197
return {
197198
regex: new RegExp(`^${regexPattern}$`, routeInfo.caseSensitive ? undefined : 'i'),
198199
and: routeInfo.and,
200+
ignoreForFallback: !!routeInfo.ignoreForFallback
199201
};
200202
}
201203
/**

0 commit comments

Comments
 (0)