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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ de-synchronizing state.
### Install the package

```bash
npm i @svelte-router/core@beta // For now, until v1.0.0 is released
npm i @svelte-router/core@beta # For now, until v1.0.0 is released
```

### Initialize the Library
Expand Down Expand Up @@ -120,8 +120,8 @@ details.
</Route>
<Route key="user" path="/users/:userId">
<!-- access parameters via the snippet parameter -->
{#snippet children(params)}
<UserView id={params.userId} /> <!-- Intellisense will work here!! -->
{#snippet children({ rp })}
<UserView id={rp?.userId} /> <!-- Intellisense will work here!! -->
{/snippet}
</Route>
...
Expand Down
22 changes: 10 additions & 12 deletions src/lib/Fallback/Fallback.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { resolveHashValue } from '$lib/kernel/resolveHashValue.js';
import { getRouterContext } from '$lib/Router/Router.svelte';
import type { RouteStatus, WhenPredicate } from '$lib/types.js';
import type { Hash, RouterChildrenContext, WhenPredicate } from '$lib/types.js';
import { assertAllowedRoutingMode } from '$lib/utils.js';
import type { Snippet } from 'svelte';

Expand Down Expand Up @@ -29,16 +29,16 @@
* {/key}
* ```
*/
hash?: boolean | string;
hash?: Hash;
/**
* Overrides the default activation conditions for the fallback content inside the component.
*
* This is useful in complex routing scenarios, where fallback content is being prevented from showing due to
* certain route or routes matching at certain points, leaving no opportunity for the router to be "out of
*
* This is useful in complex routing scenarios, where fallback content is being prevented from showing due to
* certain route or routes matching at certain points, leaving no opportunity for the router to be "out of
* matching routes".
*
*
* **This completely disconnects the `Fallback` component from the router's matching logic.**
*
*
* @example
* ```svelte
* <!--
Expand All @@ -55,11 +55,9 @@
*
* 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.
* @param state The state object stored in in the window's History API for the universe the fallback component
* is associated to.
* @param routeStatus The router's route status data.
* @param context The component's context available to children.
*/
children?: Snippet<[any, Record<string, RouteStatus>]>;
children?: Snippet<[RouterChildrenContext]>;
};

let { hash, when, children }: Props = $props();
Expand All @@ -71,5 +69,5 @@
</script>

{#if (router && when?.(router.routeStatus, router.noMatches)) || (!when && router?.noMatches)}
{@render children?.(router.state, router.routeStatus)}
{@render children?.({ state: router.state, rs: router.routeStatus })}
{/if}
130 changes: 123 additions & 7 deletions src/lib/Fallback/Fallback.svelte.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { init } from "$lib/init.js";
import { describe, test, expect, beforeAll, afterAll, beforeEach } from "vitest";
import { render } from "@testing-library/svelte";
import { createRawSnippet } from "svelte";
import Fallback from "./Fallback.svelte";
import { addMatchingRoute, addRoutes, createRouterTestSetup, createTestSnippet, ROUTING_UNIVERSES, ALL_HASHES } from "$test/test-utils.js";
import { flushSync } from "svelte";
import { resetRoutingOptions, setRoutingOptions } from "$lib/kernel/options.js";
import type { ExtendedRoutingOptions } from "$lib/types.js";
import type { ExtendedRoutingOptions, RouterChildrenContext } from "$lib/types.js";
import { location } from "$lib/kernel/Location.js";

function defaultPropsTests(setup: ReturnType<typeof createRouterTestSetup>) {
const contentText = "Fallback content.";
const content = createTestSnippet(contentText);

beforeEach(() => {
// Fresh router instance for each test
setup.init();
});

afterAll(() => {
// Clean disposal after all tests
setup.dispose();
});

test("Should render whenever the parent router matches no routes.", async () => {
// Arrange.
const { hash, router, context } = setup;
Expand All @@ -31,7 +33,7 @@ function defaultPropsTests(setup: ReturnType<typeof createRouterTestSetup>) {
// Assert.
await expect(findByText(contentText)).resolves.toBeDefined();
});

test("Should not render whenever the parent router matches at least one route.", async () => {
// Arrange.
const { hash, router, context } = setup;
Expand Down Expand Up @@ -163,6 +165,116 @@ function reactivityTests(setup: ReturnType<typeof createRouterTestSetup>) {
});
}


function fallbackChildrenSnippetContextTests(setup: ReturnType<typeof createRouterTestSetup>) {
beforeEach(() => {
// Fresh router instance for each test
setup.init();
});

afterAll(() => {
// Clean disposal after all tests
setup.dispose();
});

test("Should pass RouterChildrenContext with correct structure to children snippet when fallback activates.", async () => {
// Arrange.
const { hash, context } = setup;
let capturedContext: RouterChildrenContext;
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
capturedContext = contextObj();
return { render: () => '<div>Fallback Context Test</div>' };
});

// Act.
render(Fallback, {
props: { hash, children: content },
context
});

// Assert.
expect(capturedContext!).toBeDefined();
expect(capturedContext!).toHaveProperty('state');
expect(capturedContext!).toHaveProperty('rs');
expect(typeof capturedContext!.rs).toBe('object');
});

test("Should provide current router state in children snippet context.", async () => {
// Arrange.
const { hash, context } = setup;
let capturedContext: RouterChildrenContext;
const newState = { msg: "Test State" };
location.navigate('/', { state: newState });
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
capturedContext = contextObj();
return { render: () => '<div>Fallback State Test</div>' };
});

// Act.
render(Fallback, {
props: { hash, children: content },
context
});

// Assert.
expect(capturedContext!.state).toBeDefined();
expect(capturedContext!.state).toEqual(newState);
});

test("Should provide route status record in children snippet context.", async () => {
// Arrange.
const { hash, router, context } = setup;
let capturedContext: RouterChildrenContext;
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
capturedContext = contextObj();
return { render: () => '<div>Fallback RouteStatus Test</div>' };
});

// Add some non-matching routes to verify structure
addRoutes(router, { nonMatching: 2 });

// Act.
render(Fallback, {
props: { hash, children: content },
context
});

// Assert.
expect(capturedContext!.rs).toBeDefined();
expect(typeof capturedContext!.rs).toBe('object');
expect(Object.keys(capturedContext!.rs)).toHaveLength(2);
// Verify each route status has correct structure
Object.keys(capturedContext!.rs).forEach(key => {
expect(capturedContext?.rs[key]).toHaveProperty('match');
expect(typeof capturedContext?.rs[key].match).toBe('boolean');
});
});

test("Should not render children snippet when parent router has matching routes.", async () => {
// Arrange.
const { hash, router, context } = setup;
let capturedContext: RouterChildrenContext;
let callCount = 0;
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
capturedContext = contextObj();
callCount++;
return { render: () => '<div>Should Not Render</div>' };
});

// Add matching route to prevent fallback activation
addMatchingRoute(router);

// Act.
render(Fallback, {
props: { hash, children: content },
context
});

// Assert - snippet should not be called when routes are matching.
expect(callCount).toBe(0);
});
}

describe("Routing Mode Assertions", () => {
const contentText = "Fallback content.";
const content = createTestSnippet(contentText);
Expand Down Expand Up @@ -207,8 +319,8 @@ describe("Routing Mode Assertions", () => {

// Act & Assert
expect(() => {
render(Fallback, {
props: { hash, children: content },
render(Fallback, {
props: { hash, children: content },
});
}).toThrow();
});
Expand Down Expand Up @@ -236,5 +348,9 @@ ROUTING_UNIVERSES.forEach(ru => {
describe("Reactivity", () => {
reactivityTests(setup);
});

describe("Children Snippet Context", () => {
fallbackChildrenSnippetContextTests(setup);
});
});
});
4 changes: 2 additions & 2 deletions src/lib/Fallback/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ route status data is calculated.

| Property | Type | Default Value | Bindable | Description |
|-|-|-|-|-|
| `hash` | `boolean \| string` | `undefined` | | Sets the hash mode of the component. |
| `hash` | `Hash` | `undefined` | | Sets the hash mode of the component. |
| `when` | `WhenPredicate` | `undefined` | | Overrides the default activation conditions for the fallback content inside the component. |
| `children` | `Snippet<[any, Record<string, RouteStatus>]>` | `undefined` | | Renders the children of the component. |
| `children` | `Snippet<[RouterChildrenContext]>` | `undefined` | | Renders the children of the component. |

[Online Documentation](https://wjfe-n-savant.hashnode.space/wjfe-n-savant/components/fallback)

Expand Down
33 changes: 17 additions & 16 deletions src/lib/Link/Link.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { getLinkContext, type ILinkContext } from '$lib/LinkContext/LinkContext.svelte';
import { isRouteActive } from '$lib/public-utils.js';
import { getRouterContext } from '$lib/Router/Router.svelte';
import type { Hash, RouteStatus } from '$lib/types.js';
import type { Hash, LinkChildrenContext } from '$lib/types.js';
import { assertAllowedRoutingMode, expandAriaAttributes, joinStyles } from '$lib/utils.js';
import { type Snippet } from 'svelte';
import type { AriaAttributes, HTMLAnchorAttributes } from 'svelte/elements';
Expand Down Expand Up @@ -60,12 +60,9 @@
activeFor?: string;
/**
* Renders the children of the component.
* @param state The state object stored in in the window's History API for the universe the link is
* associated to.
* @param routeStatus The router's route status data, if the `Link` component is within the context of a
* router.
* @param context The component's context available to children.
*/
children?: Snippet<[any, Record<string, RouteStatus> | undefined]>;
children?: Snippet<[LinkChildrenContext]>;
};

let {
Expand Down Expand Up @@ -111,14 +108,18 @@
};
});
const isActive = $derived(isRouteActive(router, activeFor));
const calcHref = $derived(href === '' ? location.url.href : calculateHref(
{
hash: resolvedHash,
preserveQuery: calcPreserveQuery
},
calcPrependBasePath ? router?.basePath : undefined,
href
));
const calcHref = $derived(
href === ''
? location.url.href
: calculateHref(
{
hash: resolvedHash,
preserveQuery: calcPreserveQuery
},
calcPrependBasePath ? router?.basePath : undefined,
href
)
);

function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLAnchorElement }) {
incomingOnclick?.(event);
Expand All @@ -134,8 +135,8 @@
class={[cssClass, (isActive && calcActiveState?.class) || undefined]}
style={isActive ? joinStyles(style, calcActiveState?.style) : style}
onclick={handleClick}
{...(isActive ? calcActiveStateAria : undefined)}
{...isActive ? calcActiveStateAria : undefined}
{...restProps}
>
{@render children?.(location.getState(resolvedHash), router?.routeStatus)}
{@render children?.({ state: location.getState(resolvedHash), rs: router?.routeStatus })}
</a>
Loading