Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
14 changes: 7 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Where `RouteInfo` contains:

#### Reactive Properties
- `routeStatus`: Per-route match status and extracted parameters
- `noMatches`: Boolean indicating NO routes matched (excluding `ignoreForFallback` routes)
- `fallback`: Boolean indicating NO routes matched (excluding `ignoreForFallback` routes)

### Component Architecture

Expand All @@ -72,12 +72,12 @@ universe they belong to.
#### Fallback Component
- Shows content when no routes match
- Props:
- `when?: WhenPredicate`: Override default `noMatches` behavior
- `when?: WhenPredicate`: Override default `fallback` behavior
- `children`: Content snippet

Render logic:
```svelte
{#if (router && when?.(router.routeStatus, router.noMatches)) || (!when && router?.noMatches)}
{#if (router && when?.(router.routeStatus, router.fallback)) || (!when && router?.fallback)}
{@render children?.(router.state, router.routeStatus)}
{/if}
```
Expand Down Expand Up @@ -224,8 +224,8 @@ test("Should hide content when routes match.", () => {
});

// ❌ Bad - Test internal implementation
test("Should call router.noMatches.", () => {
const spy = vi.spyOn(router, 'noMatches');
test("Should call router.fallback.", () => {
const spy = vi.spyOn(router, 'fallback');
render(Component, { props, context });
expect(spy).toHaveBeenCalled(); // Testing implementation detail
});
Expand All @@ -240,7 +240,7 @@ test("Component renders when condition is met.", () => {
});

// ✅ Router tests focus on router logic (separate file)
test("Router calculates noMatches correctly.", () => {
test("Router calculates fallback correctly.", () => {
// Test router's internal logic
});
```
Expand Down Expand Up @@ -691,7 +691,7 @@ test("Should react to state changes.", () => {
**Purpose**: Render content when no routes match, with override capability

**Key behaviors to test**:
1. Shows when `router.noMatches` is true
1. Shows when `router.fallback` is true
2. Hides when routes are matching
3. Respects `when` predicate override
4. Works across all routing universes
Expand Down
6 changes: 3 additions & 3 deletions src/lib/Fallback/Fallback.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@
/**
* Renders the children of the component.
*
* This rendering is conditioned to the parent router engine's `noMatches` property being `true`. This means
* that the children will only be rendered when no route matches the current location.
* This rendering is conditioned to the parent router engine's `fallback` property being `true`. This means
* that the children will only be rendered when no routes match the current location.
* @param context The component's context available to children.
*/
children?: Snippet<[RouterChildrenContext]>;
Expand All @@ -68,6 +68,6 @@
const router = getRouterContext(resolvedHash);
</script>

{#if (router && when?.(router.routeStatus, router.noMatches)) || (!when && router?.noMatches)}
{#if (router && when?.(router.routeStatus, router.fallback)) || (!when && router?.fallback)}
{@render children?.({ state: router.state, rs: router.routeStatus })}
{/if}
2 changes: 1 addition & 1 deletion src/lib/Fallback/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
The `Fallback` component can be thought about as a `Route` component that only render its children if there are no
other routes in the parent router engine that match.

Internally, it checks the parent router engine's `noMatches` value, which is a reactive value calculated when all other
Internally, it checks the parent router engine's `fallback` value, which is a reactive value calculated when all other
route status data is calculated.

## Props
Expand Down
10 changes: 5 additions & 5 deletions src/lib/kernel/RouterEngine.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,13 +666,13 @@ ROUTING_UNIVERSES.forEach(universe => {
});
});

describe('noMatches', () => {
describe('fallback', () => {
test("Should be true whenever there are no routes registered.", () => {
// Act.
const router = new RouterEngine({ hash: universe.hash });

// Assert.
expect(router.noMatches).toBe(true);
expect(router.fallback).toBe(true);
});

test("Should be true whenever there are no matching routes.", () => {
Expand All @@ -686,7 +686,7 @@ ROUTING_UNIVERSES.forEach(universe => {
};

// Assert.
expect(router.noMatches).toBe(true);
expect(router.fallback).toBe(true);
});

test.each([
Expand Down Expand Up @@ -714,7 +714,7 @@ ROUTING_UNIVERSES.forEach(universe => {
addRoutes(router, { matching: routeCount, nonMatching: nonMatchingCount });

// Assert.
expect(router.noMatches).toBe(false);
expect(router.fallback).toBe(false);
});

test.each([
Expand All @@ -732,7 +732,7 @@ ROUTING_UNIVERSES.forEach(universe => {
});

// Assert.
expect(router.noMatches).toBe(true);
expect(router.fallback).toBe(true);
});
});
});
Expand Down
28 changes: 14 additions & 14 deletions src/lib/kernel/RouterEngine.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class RouterEngine {
#routeHelper;
#cleanup = false;
#parent: RouterEngine | undefined;
#resolvedHash: Hash;
resolvedHash: Hash;
/**
* Gets or sets the router's identifier. This is displayed by the `RouterTracer` component.
*/
Expand Down Expand Up @@ -86,17 +86,17 @@ export class RouterEngine {

#routeStatusData = $derived.by(() => {
const routeStatus = {} as Record<string, RouteStatus>;
let noMatches = true;
let fallback = true;
for (let routeKey of Object.keys(this.routes)) {
const pattern = this.#routePatterns.get(routeKey)!;
const [match, routeParams] = this.#routeHelper.testRoute(pattern);
noMatches = noMatches && (pattern.ignoreForFallback ? true : !match);
fallback = fallback && (pattern.ignoreForFallback ? true : !match);
routeStatus[routeKey] = {
match,
routeParams,
};
}
return [routeStatus, noMatches] as const;
return [routeStatus, fallback] as const;
});
/**
* Gets a a record of route statuses where the keys are the route keys, and the values are
Expand All @@ -105,9 +105,9 @@ export class RouterEngine {
routeStatus = $derived(this.#routeStatusData[0]);
/**
* Gets a boolean value that indicates whether the current URL matches none of the route
* patterns.
* patterns, therefore enabling fallback behavior.
*/
noMatches = $derived(this.#routeStatusData[1]);
fallback = $derived(this.#routeStatusData[1]);
/**
* Initializes a new instance of this class with the specified options.
*/
Expand All @@ -121,24 +121,24 @@ export class RouterEngine {
throw new Error("The routing library hasn't been initialized. Execute init() before creating routers.");
}
if (isRouterEngine(parentOrOpts)) {
this.#resolvedHash = parentOrOpts.#resolvedHash;
this.resolvedHash = parentOrOpts.resolvedHash;
this.#parent = parentOrOpts;
}
else {
this.#parent = parentOrOpts?.parent;
this.#resolvedHash = this.#parent && parentOrOpts?.hash === undefined ? this.#parent.#resolvedHash : resolveHashValue(parentOrOpts?.hash);
if (this.#parent && this.#resolvedHash !== this.#parent.#resolvedHash) {
this.resolvedHash = this.#parent && parentOrOpts?.hash === undefined ? this.#parent.resolvedHash : resolveHashValue(parentOrOpts?.hash);
if (this.#parent && this.resolvedHash !== this.#parent.resolvedHash) {
throw new Error("The parent router's hash mode must match the child router's hash mode.");
}
if (routingOptions.hashMode === 'multi' && this.#resolvedHash && typeof this.#resolvedHash !== 'string') {
if (routingOptions.hashMode === 'multi' && this.resolvedHash && typeof this.resolvedHash !== 'string') {
throw new Error("The specified hash value is not valid for the 'multi' hash mode. Either don't specify a hash for path routing, or correct the hash value.");
}
if (routingOptions.hashMode !== 'multi' && typeof this.#resolvedHash === 'string') {
if (routingOptions.hashMode !== 'multi' && typeof this.resolvedHash === 'string') {
throw new Error("A hash path ID was given, but is only allowed when the library's hash mode has been set to 'multi'.");
}
}
assertAllowedRoutingMode(this.#resolvedHash);
this.#routeHelper = new RouteHelper(this.#resolvedHash);
assertAllowedRoutingMode(this.resolvedHash);
this.#routeHelper = new RouteHelper(this.resolvedHash);
if (traceOptions.routerHierarchy) {
registerRouter(this);
this.#cleanup = true;
Expand All @@ -158,7 +158,7 @@ export class RouterEngine {
* This is a shortcut for `location.state`.
*/
get state() {
return location.getState(this.#resolvedHash);
return location.getState(this.resolvedHash);
}
/**
* Gets or sets the router's base path.
Expand Down
4 changes: 2 additions & 2 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,10 +428,10 @@ export type ActiveState = {
* Defines the type of function accepted by the `Fallback` component via its `when` property.
*
* @param routeStatus The current route status data from the parent router.
* @param noMatches The value that the parent router has calculated as per standard fallback logic.
* @param fallback The value that the parent router has calculated as per standard fallback logic.
* @returns `true` if the fallback content should be shown; `false` to prevent content from being shown.
*/
export type WhenPredicate = (routeStatus: Record<string, RouteStatus>, noMatches: boolean) => boolean;
export type WhenPredicate = (routeStatus: Record<string, RouteStatus>, fallback: boolean) => boolean;

/**
* Defines the shape of logger objects that can be given to this library during initialization.
Expand Down