Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/Route/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<RouteParameters<T>, ParameterValue> \| undefined) => boolean` | `undefined` | | Sets a function for additional matching conditions. |
| `when` | `(routeStatus: Record<string, RouteStatus>) => 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<RouteParameters<T>, ParameterValue>` | `undefined` | Yes | Provides a way to obtain a route's parameters through property binding. |
Expand Down
37 changes: 8 additions & 29 deletions src/lib/Route/Route.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -71,32 +71,11 @@
*/
and?: (params: Record<RouteParameters<T>, 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)}
* <Route ...>...</Route>
* {/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
* <Route key="notHome" when={({ home }) => !home.match}>
* <NotHome />
* </Route>
* ```
* If `true`, the route will not be considered when determining fallback content visibility.
*/
when?: (routeStatus: Record<string, RouteStatus>) => boolean;
ignoreForFallback?: boolean;
/**
* Sets whether the route's path pattern should be matched case-sensitively.
*
Expand Down Expand Up @@ -146,7 +125,7 @@
key,
path,
and,
when,
ignoreForFallback = false,
caseSensitive = false,
hash,
params = $bindable(),
Expand All @@ -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 () => {
Expand All @@ -186,6 +165,6 @@
});
</script>

{#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}
73 changes: 70 additions & 3 deletions src/lib/core/RouterEngine.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -39,7 +39,7 @@ describe("RouterEngine", () => {
});
});

describe("RouterEngine", () => {
describe("RouterEngine (default init)", () => {
let _href: string;
let cleanup: () => void;
let interceptedState: any = null;
Expand Down Expand Up @@ -73,6 +73,9 @@ describe("RouterEngine", () => {
replaceState: replaceStateMock
};
});
beforeEach(() => {
location.url.href = globalThis.window.location.href = "http://example.com";
});
afterAll(() => {
cleanup();
});
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 6 additions & 4 deletions src/lib/core/RouterEngine.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, { regex?: RegExp; and?: AndUntyped; }>()));
}, new Map<string, { regex?: RegExp; and?: AndUntyped; ignoreForFallback: boolean; }>()));

[routePatternsKey]() {
return this.#routePatterns;
Expand All @@ -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,
Expand All @@ -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);
Expand All @@ -196,6 +197,7 @@ export class RouterEngine {
return {
regex: new RegExp(`^${regexPattern}$`, routeInfo.caseSensitive ? undefined : 'i'),
and: routeInfo.and,
ignoreForFallback: !!routeInfo.ignoreForFallback
};
}
/**
Expand Down
11 changes: 2 additions & 9 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,6 @@ export type RouteStatus = {
*/
export type AndUntyped = (params: Record<string, ParameterValue> | 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<string, RouteStatus>) => boolean

/**
* Defines the core properties of a route definition.
*/
Expand All @@ -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;
}

/**
Expand Down