Skip to content

Commit 4c83187

Browse files
committed
feat!: Group children parameters together
1 parent 6a8e4b8 commit 4c83187

17 files changed

+762
-186
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ de-synchronizing state.
6060
### Install the package
6161

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

6666
### Initialize the Library
@@ -120,8 +120,8 @@ details.
120120
</Route>
121121
<Route key="user" path="/users/:userId">
122122
<!-- access parameters via the snippet parameter -->
123-
{#snippet children(params)}
124-
<UserView id={params.userId} /> <!-- Intellisense will work here!! -->
123+
{#snippet children({ rp })}
124+
<UserView id={rp?.userId} /> <!-- Intellisense will work here!! -->
125125
{/snippet}
126126
</Route>
127127
...

src/lib/Fallback/Fallback.svelte

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import { resolveHashValue } from '$lib/kernel/resolveHashValue.js';
33
import { getRouterContext } from '$lib/Router/Router.svelte';
4-
import type { RouteStatus, WhenPredicate } from '$lib/types.js';
4+
import type { Hash, RouterChildrenContext, WhenPredicate } from '$lib/types.js';
55
import { assertAllowedRoutingMode } from '$lib/utils.js';
66
import type { Snippet } from 'svelte';
77
@@ -29,16 +29,16 @@
2929
* {/key}
3030
* ```
3131
*/
32-
hash?: boolean | string;
32+
hash?: Hash;
3333
/**
3434
* Overrides the default activation conditions for the fallback content inside the component.
35-
*
36-
* This is useful in complex routing scenarios, where fallback content is being prevented from showing due to
37-
* certain route or routes matching at certain points, leaving no opportunity for the router to be "out of
35+
*
36+
* This is useful in complex routing scenarios, where fallback content is being prevented from showing due to
37+
* certain route or routes matching at certain points, leaving no opportunity for the router to be "out of
3838
* matching routes".
39-
*
39+
*
4040
* **This completely disconnects the `Fallback` component from the router's matching logic.**
41-
*
41+
*
4242
* @example
4343
* ```svelte
4444
* <!--
@@ -55,11 +55,9 @@
5555
*
5656
* This rendering is conditioned to the parent router engine's `noMatches` property being `true`. This means
5757
* that the children will only be rendered when no route matches the current location.
58-
* @param state The state object stored in in the window's History API for the universe the fallback component
59-
* is associated to.
60-
* @param routeStatus The router's route status data.
58+
* @param context The component's context available to children.
6159
*/
62-
children?: Snippet<[any, Record<string, RouteStatus>]>;
60+
children?: Snippet<[RouterChildrenContext]>;
6361
};
6462
6563
let { hash, when, children }: Props = $props();
@@ -71,5 +69,5 @@
7169
</script>
7270

7371
{#if (router && when?.(router.routeStatus, router.noMatches)) || (!when && router?.noMatches)}
74-
{@render children?.(router.state, router.routeStatus)}
72+
{@render children?.({ state: router.state, rs: router.routeStatus })}
7573
{/if}

src/lib/Fallback/Fallback.svelte.test.ts

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import { init } from "$lib/init.js";
22
import { describe, test, expect, beforeAll, afterAll, beforeEach } from "vitest";
33
import { render } from "@testing-library/svelte";
4+
import { createRawSnippet } from "svelte";
45
import Fallback from "./Fallback.svelte";
56
import { addMatchingRoute, addRoutes, createRouterTestSetup, createTestSnippet, ROUTING_UNIVERSES, ALL_HASHES } from "$test/test-utils.js";
67
import { flushSync } from "svelte";
78
import { resetRoutingOptions, setRoutingOptions } from "$lib/kernel/options.js";
8-
import type { ExtendedRoutingOptions } from "$lib/types.js";
9+
import type { ExtendedRoutingOptions, RouterChildrenContext } from "$lib/types.js";
10+
import { location } from "$lib/kernel/Location.js";
911

1012
function defaultPropsTests(setup: ReturnType<typeof createRouterTestSetup>) {
1113
const contentText = "Fallback content.";
1214
const content = createTestSnippet(contentText);
13-
15+
1416
beforeEach(() => {
1517
// Fresh router instance for each test
1618
setup.init();
1719
});
18-
20+
1921
afterAll(() => {
2022
// Clean disposal after all tests
2123
setup.dispose();
2224
});
23-
25+
2426
test("Should render whenever the parent router matches no routes.", async () => {
2527
// Arrange.
2628
const { hash, router, context } = setup;
@@ -31,7 +33,7 @@ function defaultPropsTests(setup: ReturnType<typeof createRouterTestSetup>) {
3133
// Assert.
3234
await expect(findByText(contentText)).resolves.toBeDefined();
3335
});
34-
36+
3537
test("Should not render whenever the parent router matches at least one route.", async () => {
3638
// Arrange.
3739
const { hash, router, context } = setup;
@@ -163,6 +165,116 @@ function reactivityTests(setup: ReturnType<typeof createRouterTestSetup>) {
163165
});
164166
}
165167

168+
169+
function fallbackChildrenSnippetContextTests(setup: ReturnType<typeof createRouterTestSetup>) {
170+
beforeEach(() => {
171+
// Fresh router instance for each test
172+
setup.init();
173+
});
174+
175+
afterAll(() => {
176+
// Clean disposal after all tests
177+
setup.dispose();
178+
});
179+
180+
test("Should pass RouterChildrenContext with correct structure to children snippet when fallback activates.", async () => {
181+
// Arrange.
182+
const { hash, context } = setup;
183+
let capturedContext: RouterChildrenContext;
184+
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
185+
capturedContext = contextObj();
186+
return { render: () => '<div>Fallback Context Test</div>' };
187+
});
188+
189+
// Act.
190+
render(Fallback, {
191+
props: { hash, children: content },
192+
context
193+
});
194+
195+
// Assert.
196+
expect(capturedContext!).toBeDefined();
197+
expect(capturedContext!).toHaveProperty('state');
198+
expect(capturedContext!).toHaveProperty('rs');
199+
expect(typeof capturedContext!.rs).toBe('object');
200+
});
201+
202+
test("Should provide current router state in children snippet context.", async () => {
203+
// Arrange.
204+
const { hash, context } = setup;
205+
let capturedContext: RouterChildrenContext;
206+
const newState = { msg: "Test State" };
207+
location.navigate('/', { state: newState });
208+
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
209+
capturedContext = contextObj();
210+
return { render: () => '<div>Fallback State Test</div>' };
211+
});
212+
213+
// Act.
214+
render(Fallback, {
215+
props: { hash, children: content },
216+
context
217+
});
218+
219+
// Assert.
220+
expect(capturedContext!.state).toBeDefined();
221+
expect(capturedContext!.state).toEqual(newState);
222+
});
223+
224+
test("Should provide route status record in children snippet context.", async () => {
225+
// Arrange.
226+
const { hash, router, context } = setup;
227+
let capturedContext: RouterChildrenContext;
228+
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
229+
capturedContext = contextObj();
230+
return { render: () => '<div>Fallback RouteStatus Test</div>' };
231+
});
232+
233+
// Add some non-matching routes to verify structure
234+
addRoutes(router, { nonMatching: 2 });
235+
236+
// Act.
237+
render(Fallback, {
238+
props: { hash, children: content },
239+
context
240+
});
241+
242+
// Assert.
243+
expect(capturedContext!.rs).toBeDefined();
244+
expect(typeof capturedContext!.rs).toBe('object');
245+
expect(Object.keys(capturedContext!.rs)).toHaveLength(2);
246+
// Verify each route status has correct structure
247+
Object.keys(capturedContext!.rs).forEach(key => {
248+
expect(capturedContext?.rs[key]).toHaveProperty('match');
249+
expect(typeof capturedContext?.rs[key].match).toBe('boolean');
250+
});
251+
});
252+
253+
test("Should not render children snippet when parent router has matching routes.", async () => {
254+
// Arrange.
255+
const { hash, router, context } = setup;
256+
let capturedContext: RouterChildrenContext;
257+
let callCount = 0;
258+
const content = createRawSnippet<[RouterChildrenContext]>((contextObj) => {
259+
capturedContext = contextObj();
260+
callCount++;
261+
return { render: () => '<div>Should Not Render</div>' };
262+
});
263+
264+
// Add matching route to prevent fallback activation
265+
addMatchingRoute(router);
266+
267+
// Act.
268+
render(Fallback, {
269+
props: { hash, children: content },
270+
context
271+
});
272+
273+
// Assert - snippet should not be called when routes are matching.
274+
expect(callCount).toBe(0);
275+
});
276+
}
277+
166278
describe("Routing Mode Assertions", () => {
167279
const contentText = "Fallback content.";
168280
const content = createTestSnippet(contentText);
@@ -207,8 +319,8 @@ describe("Routing Mode Assertions", () => {
207319

208320
// Act & Assert
209321
expect(() => {
210-
render(Fallback, {
211-
props: { hash, children: content },
322+
render(Fallback, {
323+
props: { hash, children: content },
212324
});
213325
}).toThrow();
214326
});
@@ -236,5 +348,9 @@ ROUTING_UNIVERSES.forEach(ru => {
236348
describe("Reactivity", () => {
237349
reactivityTests(setup);
238350
});
351+
352+
describe("Children Snippet Context", () => {
353+
fallbackChildrenSnippetContextTests(setup);
354+
});
239355
});
240356
});

src/lib/Fallback/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ route status data is calculated.
1010

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

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

src/lib/Link/Link.svelte

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { getLinkContext, type ILinkContext } from '$lib/LinkContext/LinkContext.svelte';
77
import { isRouteActive } from '$lib/public-utils.js';
88
import { getRouterContext } from '$lib/Router/Router.svelte';
9-
import type { Hash, RouteStatus } from '$lib/types.js';
9+
import type { Hash, LinkChildrenContext } from '$lib/types.js';
1010
import { assertAllowedRoutingMode, expandAriaAttributes, joinStyles } from '$lib/utils.js';
1111
import { type Snippet } from 'svelte';
1212
import type { AriaAttributes, HTMLAnchorAttributes } from 'svelte/elements';
@@ -60,12 +60,9 @@
6060
activeFor?: string;
6161
/**
6262
* Renders the children of the component.
63-
* @param state The state object stored in in the window's History API for the universe the link is
64-
* associated to.
65-
* @param routeStatus The router's route status data, if the `Link` component is within the context of a
66-
* router.
63+
* @param context The component's context available to children.
6764
*/
68-
children?: Snippet<[any, Record<string, RouteStatus> | undefined]>;
65+
children?: Snippet<[LinkChildrenContext]>;
6966
};
7067
7168
let {
@@ -111,14 +108,18 @@
111108
};
112109
});
113110
const isActive = $derived(isRouteActive(router, activeFor));
114-
const calcHref = $derived(href === '' ? location.url.href : calculateHref(
115-
{
116-
hash: resolvedHash,
117-
preserveQuery: calcPreserveQuery
118-
},
119-
calcPrependBasePath ? router?.basePath : undefined,
120-
href
121-
));
111+
const calcHref = $derived(
112+
href === ''
113+
? location.url.href
114+
: calculateHref(
115+
{
116+
hash: resolvedHash,
117+
preserveQuery: calcPreserveQuery
118+
},
119+
calcPrependBasePath ? router?.basePath : undefined,
120+
href
121+
)
122+
);
122123
123124
function handleClick(event: MouseEvent & { currentTarget: EventTarget & HTMLAnchorElement }) {
124125
incomingOnclick?.(event);
@@ -134,8 +135,8 @@
134135
class={[cssClass, (isActive && calcActiveState?.class) || undefined]}
135136
style={isActive ? joinStyles(style, calcActiveState?.style) : style}
136137
onclick={handleClick}
137-
{...(isActive ? calcActiveStateAria : undefined)}
138+
{...isActive ? calcActiveStateAria : undefined}
138139
{...restProps}
139140
>
140-
{@render children?.(location.getState(resolvedHash), router?.routeStatus)}
141+
{@render children?.({ state: location.getState(resolvedHash), rs: router?.routeStatus })}
141142
</a>

0 commit comments

Comments
 (0)