From 9f1e6acfe6f7730a732dfc5f92b85f32b5baf4ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramirez=20Vargas=2C=20Jos=C3=A9=20Pablo?= Date: Wed, 3 Sep 2025 23:05:05 -0600 Subject: [PATCH 1/3] feat: Add ignoreForFallback property to Route This also removes the when property. Fixes #20. --- src/lib/Route/README.md | 2 +- src/lib/Route/Route.svelte | 37 +++--------- src/lib/core/RouterEngine.svelte.test.ts | 73 +++++++++++++++++++++++- src/lib/core/RouterEngine.svelte.ts | 10 ++-- src/lib/types.ts | 11 +--- 5 files changed, 87 insertions(+), 46 deletions(-) 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..f451b2d 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,73 @@ 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, + }; + console.debug('Path:', router.path); + + // 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; } /** From fbcadd074be0e93677ed617efca7edd14e0c7770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pablo=20Ram=C3=ADrez=20Vargas?= Date: Wed, 3 Sep 2025 23:13:30 -0600 Subject: [PATCH 2/3] Remove logging from test file Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/lib/core/RouterEngine.svelte.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib/core/RouterEngine.svelte.test.ts b/src/lib/core/RouterEngine.svelte.test.ts index f451b2d..1468bc1 100644 --- a/src/lib/core/RouterEngine.svelte.test.ts +++ b/src/lib/core/RouterEngine.svelte.test.ts @@ -515,7 +515,6 @@ describe("RouterEngine (default init)", () => { pattern: '/:one/:two?', caseSensitive: false, }; - console.debug('Path:', router.path); // Assert. expect(router.noMatches).toBe(true); From 4b02a3464505a28c5b1ce3f351f27a60f920243b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ramirez=20Vargas=2C=20Jos=C3=A9=20Pablo?= Date: Thu, 4 Sep 2025 11:43:21 -0600 Subject: [PATCH 3/3] demo: Remove the use of Route.when This has restored functionality to the Fallback content in the demo. --- demo/src/App.svelte | 58 +++++++++++++++++++------------------ demo/src/lib/NavBar.svelte | 59 ++++++++++++++++++++++---------------- 2 files changed, 65 insertions(+), 52 deletions(-) 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 @@