diff --git a/demo/src/App.svelte b/demo/src/App.svelte
index 73e1303..a3abe10 100644
--- a/demo/src/App.svelte
+++ b/demo/src/App.svelte
@@ -15,12 +15,12 @@
const timer = setTimeout(() => {
showNavTooltip = true;
}, 2000);
-
+
// Hide tooltip after 10 seconds or when user interacts
const hideTimer = setTimeout(() => {
showNavTooltip = false;
}, 12000);
-
+
return () => {
clearTimeout(timer);
clearTimeout(hideTimer);
@@ -31,33 +31,35 @@
-
- {#snippet reference(ref)}
-
- {/snippet}
- Use these navigation links to test-drive the routing capabilities of @wjfe/n-savant.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {#snippet children(_, rs)}
+
+ {#snippet reference(ref)}
+
+ {/snippet}
+ Use these navigation links to test-drive the routing capabilities of @wjfe/n-savant.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
!rs.home.match}>
-
-
+
+ {#if !rs.home.match}
+
+ {/if}
+ {/snippet}
diff --git a/demo/src/lib/NavBar.svelte b/demo/src/lib/NavBar.svelte
index 3cabed2..d4cbc6b 100644
--- a/demo/src/lib/NavBar.svelte
+++ b/demo/src/lib/NavBar.svelte
@@ -4,9 +4,7 @@
import { routingMode } from './hash-routing';
import type { HTMLAttributes } from 'svelte/elements';
- let {
- ...restProps
- }: HTMLAttributes
= $props();
+ let { ...restProps }: HTMLAttributes = $props();
const pathRoutingLinks = [
{ text: 'Home', href: '/path-routing' },
@@ -62,32 +60,45 @@
-
- Home
+ Home
- !rs.pathRouting?.match}>
- -
-
- Path Routing
-
-
+
+ {#snippet children(rp, _, rs)}
+ {#if !rs.pathRouting?.match}
+ -
+
+ Path Routing
+
+
+ {/if}
+ {/snippet}
- !rs.hashRouting?.match}>
- -
-
- Hash Routing
-
-
+
+ {#snippet children(rp, _, rs)}
+ {#if !rs.hashRouting?.match}
+ -
+
+ Hash Routing
+
+
+ {/if}
+ {/snippet}
diff --git a/src/lib/Route/README.md b/src/lib/Route/README.md
index cbf7bd3..b8d5128 100644
--- a/src/lib/Route/README.md
+++ b/src/lib/Route/README.md
@@ -13,7 +13,7 @@ they can be embedded anywhere down the hierarchy, including being children of ot
| `key` | `string` | (none) | | Sets the route's unique key. |
| `path` | `string \| RegExp` | (none) | | Sets the route's path pattern, or a regular expression used to test and match the browser's URL. |
| `and` | `(params: Record, ParameterValue> \| undefined) => boolean` | `undefined` | | Sets a function for additional matching conditions. |
-| `when` | `(routeStatus: Record) => boolean` | `undefined` | | Sets a function for additional matching conditions. |
+| `ignoreForFallback` | `boolean` | `false` | | Controls whether the matching status of this route affects the visibility of fallback content. |
| `caseSensitive` | `boolean` | `false` | | Sets whether the route's path pattern should be matched case-sensitively. |
| `hash` | `boolean \| string` | `undefined` | | Sets the hash mode of the route. |
| `params` | `Record, ParameterValue>` | `undefined` | Yes | Provides a way to obtain a route's parameters through property binding. |
diff --git a/src/lib/Route/Route.svelte b/src/lib/Route/Route.svelte
index 41d0b7e..cb82add 100644
--- a/src/lib/Route/Route.svelte
+++ b/src/lib/Route/Route.svelte
@@ -71,32 +71,11 @@
*/
and?: (params: Record, ParameterValue> | undefined) => boolean;
/**
- * Sets a function for additional matching conditions.
- *
- * Use this one when you need to match based on the final status of all routes.
- * @param routeStatus The router's route status object.
- * @returns `true` if the route should match, or `false` otherwise.
+ * Sets whether the route's match status should be ignored for fallback purposes.
*
- * This is shorthand for:
- *
- * ```svelte
- * {#if when(router.routeStatus)}
- * ...
- * {/if}
- * ```
- *
- *
- * In other words, use it to further condition rendering based on the final status of all routes.
- *
- * Example: Match only if the home route did not:
- *
- * ```svelte
- * !home.match}>
- *
- *
- * ```
+ * If `true`, the route will not be considered when determining fallback content visibility.
*/
- when?: (routeStatus: Record) => boolean;
+ ignoreForFallback?: boolean;
/**
* Sets whether the route's path pattern should be matched case-sensitively.
*
@@ -146,7 +125,7 @@
key,
path,
and,
- when,
+ ignoreForFallback = false,
caseSensitive = false,
hash,
params = $bindable(),
@@ -162,17 +141,17 @@
// Effect that updates the route object in the parent router.
$effect.pre(() => {
- if (!path && !and && !when) {
+ if (!path && !and) {
return;
}
// svelte-ignore ownership_invalid_mutation
untrack(() => router.routes)[key] =
path instanceof RegExp
- ? { regex: path, and, when }
+ ? { regex: path, and, ignoreForFallback }
: {
pattern: path,
and,
- when,
+ ignoreForFallback,
caseSensitive
};
return () => {
@@ -186,6 +165,6 @@
});
-{#if (router.routeStatus[key]?.match ?? true) && (untrack(() => router.routes)[key]?.when?.(router.routeStatus) ?? true)}
+{#if (router.routeStatus[key]?.match ?? true)}
{@render children?.(params, router.state, router.routeStatus)}
{/if}
diff --git a/src/lib/core/RouterEngine.svelte.test.ts b/src/lib/core/RouterEngine.svelte.test.ts
index c542113..1468bc1 100644
--- a/src/lib/core/RouterEngine.svelte.test.ts
+++ b/src/lib/core/RouterEngine.svelte.test.ts
@@ -1,4 +1,4 @@
-import { describe, test, expect, beforeAll, afterAll, vi } from "vitest";
+import { describe, test, expect, beforeAll, afterAll, vi, beforeEach } from "vitest";
import { routePatternsKey, RouterEngine } from "./RouterEngine.svelte.js";
import { init, type Hash, type RouteInfo } from "$lib/index.js";
import { registerRouter } from "./trace.svelte.js";
@@ -39,7 +39,7 @@ describe("RouterEngine", () => {
});
});
-describe("RouterEngine", () => {
+describe("RouterEngine (default init)", () => {
let _href: string;
let cleanup: () => void;
let interceptedState: any = null;
@@ -73,6 +73,9 @@ describe("RouterEngine", () => {
replaceState: replaceStateMock
};
});
+ beforeEach(() => {
+ location.url.href = globalThis.window.location.href = "http://example.com";
+ });
afterAll(() => {
cleanup();
});
@@ -497,9 +500,72 @@ describe("RouterEngine", () => {
});
});
});
+ describe('noMatches', () => {
+ test("Should be true whenever there are no routes registered.", () => {
+ // Act.
+ const router = new RouterEngine();
+
+ // Assert.
+ expect(router.noMatches).toBe(true);
+ });
+ test("Should be true whenever there are no matching routes.", () => {
+ // Act.
+ const router = new RouterEngine();
+ router.routes['route'] = {
+ pattern: '/:one/:two?',
+ caseSensitive: false,
+ };
+
+ // Assert.
+ expect(router.noMatches).toBe(true);
+ });
+ test.each([
+ {
+ text: "is",
+ routeCount: 1,
+ totalRoutes: 5
+ },
+ {
+ text: "are",
+ routeCount: 2,
+ totalRoutes: 5
+ },
+ {
+ text: "are",
+ routeCount: 5,
+ totalRoutes: 5
+ },
+ ])("Should be false whenever there $text $routeCount matching route(s) out of $totalRoutes route(s).", ({ routeCount, totalRoutes }) => {
+ // Act.
+ const router = new RouterEngine();
+ for (let i = 0; i < routeCount; i++) {
+ router.routes[`route${i}`] = {
+ and: () => i < routeCount
+ };
+ }
+
+ // Assert.
+ expect(router.noMatches).toBe(false);
+ });
+ test.each([
+ 1, 2, 5
+ ])("Should be true whenever the %d matching route(s) are ignored for fallback.", (routeCount) => {
+ // Act.
+ const router = new RouterEngine();
+ for (let i = 0; i < routeCount; i++) {
+ router.routes[`route${i}`] = {
+ and: () => true,
+ ignoreForFallback: true
+ };
+ }
+
+ // Assert.
+ expect(router.noMatches).toBe(true);
+ });
+ });
});
-describe("RouterEngine", () => {
+describe("RouterEngine (multi hash)", () => {
let _href: string;
let cleanup: () => void;
let interceptedState: any = null;
diff --git a/src/lib/core/RouterEngine.svelte.ts b/src/lib/core/RouterEngine.svelte.ts
index c53d95a..61bf9e9 100644
--- a/src/lib/core/RouterEngine.svelte.ts
+++ b/src/lib/core/RouterEngine.svelte.ts
@@ -127,11 +127,11 @@ export class RouterEngine {
#routePatterns = $derived(Object.entries(this.routes).reduce((map, [key, route]) => {
map.set(
key, routeInfoIsRegexInfo(route) ?
- { regex: route.regex, and: route.and } :
+ { regex: route.regex, and: route.and, ignoreForFallback: !!route.ignoreForFallback } :
this.#parseRoutePattern(route)
);
return map;
- }, new Map()));
+ }, new Map()));
[routePatternsKey]() {
return this.#routePatterns;
@@ -156,7 +156,7 @@ export class RouterEngine {
}
}
const match = (!!matches || !pattern.regex) && (!pattern.and || pattern.and(routeParams));
- noMatches = noMatches && !match;
+ noMatches = noMatches && (pattern.ignoreForFallback ? true : !match);
routeStatus[routeKey] = {
match,
routeParams,
@@ -179,10 +179,11 @@ export class RouterEngine {
* @param routeInfo Pattern route information to parse.
* @returns An object with the regular expression and the optional predicate function.
*/
- #parseRoutePattern(routeInfo: PatternRouteInfo): { regex?: RegExp; and?: AndUntyped; } {
+ #parseRoutePattern(routeInfo: PatternRouteInfo): { regex?: RegExp; and?: AndUntyped; ignoreForFallback: boolean; } {
if (!routeInfo.pattern) {
return {
and: routeInfo.and,
+ ignoreForFallback: !!routeInfo.ignoreForFallback
}
}
const fullPattern = joinPaths(this.basePath, routeInfo.pattern === '/' ? '' : routeInfo.pattern);
@@ -196,6 +197,7 @@ export class RouterEngine {
return {
regex: new RegExp(`^${regexPattern}$`, routeInfo.caseSensitive ? undefined : 'i'),
and: routeInfo.and,
+ ignoreForFallback: !!routeInfo.ignoreForFallback
};
}
/**
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 4510751..83bfaa7 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -56,12 +56,6 @@ export type RouteStatus = {
*/
export type AndUntyped = (params: Record | undefined) => boolean;
-/**
- * Defines the shape of predicate functions that are used to determine if the route contents should show based on the
- * route status information of all routes in the router.
- */
-export type WhenPredicate = (routeStatus: Record) => boolean
-
/**
* Defines the core properties of a route definition.
*/
@@ -71,10 +65,9 @@ export type CoreRouteInfo = {
*/
and?: AndUntyped;
/**
- * An optional predicate function that is used to determine if the route contents should show based on the route
- * status information of all routes in the router.
+ * A Boolean value that determines if the route's match status should be ignored for fallback purposes.
*/
- when?: WhenPredicate;
+ ignoreForFallback?: boolean;
}
/**