@real-router/svelte 0.2.13 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/RouterProvider.svelte +3 -17
- package/dist/actions/link.svelte.d.ts +3 -1
- package/dist/actions/link.svelte.js +14 -16
- package/dist/components/Lazy.svelte +23 -15
- package/dist/components/Link.svelte +4 -3
- package/dist/components/RouteView.svelte +24 -19
- package/dist/components/RouteView.svelte.d.ts +1 -0
- package/dist/components/RouterErrorBoundary.svelte +20 -20
- package/dist/composables/useIsActiveRoute.svelte.d.ts +1 -1
- package/dist/composables/useIsActiveRoute.svelte.js +1 -1
- package/dist/composables/useNavigator.svelte.js +2 -9
- package/dist/composables/useRoute.svelte.js +2 -9
- package/dist/composables/useRouteNode.svelte.js +2 -17
- package/dist/composables/useRouter.svelte.js +2 -9
- package/dist/composables/useRouterTransition.svelte.js +2 -2
- package/dist/constants.d.ts +4 -0
- package/dist/constants.js +3 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.js +13 -0
- package/dist/createRouteContext.svelte.d.ts +9 -0
- package/dist/createRouteContext.svelte.js +13 -0
- package/dist/dom-utils/index.d.ts +1 -1
- package/dist/dom-utils/index.js +1 -1
- package/dist/dom-utils/link-utils.d.ts +2 -1
- package/dist/dom-utils/link-utils.js +48 -6
- package/dist/dom-utils/route-announcer.js +37 -14
- package/package.json +5 -5
- package/src/RouterProvider.svelte +3 -17
- package/src/actions/link.svelte.ts +27 -29
- package/src/components/Lazy.svelte +23 -15
- package/src/components/Link.svelte +4 -3
- package/src/components/RouteView.svelte +24 -19
- package/src/components/RouterErrorBoundary.svelte +20 -20
- package/src/composables/useIsActiveRoute.svelte.ts +3 -3
- package/src/composables/useNavigator.svelte.ts +3 -12
- package/src/composables/useRoute.svelte.ts +3 -12
- package/src/composables/useRouteNode.svelte.ts +2 -17
- package/src/composables/useRouter.svelte.ts +3 -12
- package/src/composables/useRouterTransition.svelte.ts +2 -3
- package/src/constants.ts +9 -0
- package/src/context.ts +17 -0
- package/src/createRouteContext.svelte.ts +27 -0
- package/dist/composables/useRouterError.svelte.d.ts +0 -4
- package/dist/composables/useRouterError.svelte.js +0 -13
- package/src/composables/useRouterError.svelte.ts +0 -21
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ Navigation link with automatic active state detection. Uses `$derived` for href
|
|
|
142
142
|
| `activeStrict` | `boolean` | `false` | Exact match only (no ancestor matching) |
|
|
143
143
|
| `ignoreQueryParams` | `boolean` | `true` | Query params don't affect active state |
|
|
144
144
|
| `target` | `string` | `undefined` | Link target (`_blank`, etc.) |
|
|
145
|
+
| `onclick` | `(evt: MouseEvent) => void` | `undefined` | Custom click handler. Runs **before** the navigation logic — call `evt.preventDefault()` to suppress navigation. |
|
|
145
146
|
|
|
146
147
|
All other props are spread onto the `<a>` element.
|
|
147
148
|
|
|
@@ -206,7 +207,13 @@ Declarative error handling for navigation errors. Shows a fallback **alongside**
|
|
|
206
207
|
</script>
|
|
207
208
|
|
|
208
209
|
<RouterErrorBoundary
|
|
209
|
-
onError={(error
|
|
210
|
+
onError={(error, toRoute, fromRoute) =>
|
|
211
|
+
analytics.track("nav_error", {
|
|
212
|
+
code: error.code,
|
|
213
|
+
to: toRoute?.name,
|
|
214
|
+
from: fromRoute?.name,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
210
217
|
>
|
|
211
218
|
{#snippet fallback(error, resetError)}
|
|
212
219
|
<div class="toast">
|
|
@@ -220,6 +227,8 @@ Declarative error handling for navigation errors. Shows a fallback **alongside**
|
|
|
220
227
|
|
|
221
228
|
Auto-resets on next successful navigation. Works with both `<Link>` and imperative `router.navigate()`.
|
|
222
229
|
|
|
230
|
+
**`onError` signature:** `(error, toRoute, fromRoute) => void`. Receives the `RouterError`, the attempted destination (`State | null`), and the previously active route (`State | null`). A throwing `onError` is caught by the boundary, logged via `console.error`, and never breaks reactivity.
|
|
231
|
+
|
|
223
232
|
## Actions
|
|
224
233
|
|
|
225
234
|
### `createLinkAction`
|
|
@@ -363,9 +372,9 @@ Full documentation: [Wiki](https://github.com/greydragon888/real-router/wiki)
|
|
|
363
372
|
|
|
364
373
|
## Examples
|
|
365
374
|
|
|
366
|
-
|
|
375
|
+
16 runnable examples — each is a standalone Vite app. Run: `cd examples/svelte/basic && pnpm dev`
|
|
367
376
|
|
|
368
|
-
[basic](../../examples/svelte/basic) · [nested-routes](../../examples/svelte/nested-routes) · [auth-guards](../../examples/svelte/auth-guards) · [data-loading](../../examples/svelte/data-loading) · [lazy-loading](../../examples/svelte/lazy-loading) · [async-guards](../../examples/svelte/async-guards) · [hash-routing](../../examples/svelte/hash-routing) · [persistent-params](../../examples/svelte/persistent-params) · [error-handling](../../examples/svelte/error-handling) · [dynamic-routes](../../examples/svelte/dynamic-routes) · [link-action](../../examples/svelte/link-action) · [lazy-loading-svelte](../../examples/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/svelte/snippets-routing) · [reactive-source](../../examples/svelte/reactive-source) · [combined](../../examples/svelte/combined)
|
|
377
|
+
[basic](../../examples/svelte/basic) · [nested-routes](../../examples/svelte/nested-routes) · [auth-guards](../../examples/svelte/auth-guards) · [data-loading](../../examples/svelte/data-loading) · [lazy-loading](../../examples/svelte/lazy-loading) · [async-guards](../../examples/svelte/async-guards) · [hash-routing](../../examples/svelte/hash-routing) · [persistent-params](../../examples/svelte/persistent-params) · [error-handling](../../examples/svelte/error-handling) · [dynamic-routes](../../examples/svelte/dynamic-routes) · [link-action](../../examples/svelte/link-action) · [lazy-loading-svelte](../../examples/svelte/lazy-loading-svelte) · [snippets-routing](../../examples/svelte/snippets-routing) · [reactive-source](../../examples/svelte/reactive-source) · [search-schema](../../examples/svelte/search-schema) · [combined](../../examples/svelte/combined)
|
|
369
378
|
|
|
370
379
|
## Related Packages
|
|
371
380
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { setContext } from "svelte";
|
|
6
6
|
|
|
7
7
|
import { createReactiveSource } from "./createReactiveSource.svelte";
|
|
8
|
+
import { createRouteContext } from "./createRouteContext.svelte";
|
|
8
9
|
import { NAVIGATOR_KEY, ROUTE_KEY, ROUTER_KEY } from "./context";
|
|
9
10
|
|
|
10
11
|
import type { Router } from "@real-router/core";
|
|
@@ -26,26 +27,11 @@
|
|
|
26
27
|
const navigator = getNavigator(router);
|
|
27
28
|
const source = createRouteSource(router);
|
|
28
29
|
const reactive = createReactiveSource(source);
|
|
30
|
+
const routeContext = createRouteContext(navigator, reactive);
|
|
29
31
|
|
|
30
32
|
setContext(ROUTER_KEY, router);
|
|
31
33
|
setContext(NAVIGATOR_KEY, navigator);
|
|
32
|
-
setContext(ROUTE_KEY,
|
|
33
|
-
navigator,
|
|
34
|
-
get route() {
|
|
35
|
-
return {
|
|
36
|
-
get current() {
|
|
37
|
-
return reactive.current.route;
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
get previousRoute() {
|
|
42
|
-
return {
|
|
43
|
-
get current() {
|
|
44
|
-
return reactive.current.previousRoute;
|
|
45
|
-
},
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
});
|
|
34
|
+
setContext(ROUTE_KEY, routeContext);
|
|
49
35
|
</script>
|
|
50
36
|
|
|
51
37
|
{@render children()}
|
|
@@ -5,6 +5,7 @@ export interface LinkActionParams {
|
|
|
5
5
|
params?: Params;
|
|
6
6
|
options?: NavigationOptions;
|
|
7
7
|
}
|
|
8
|
+
type LinkAction = (node: HTMLElement, params: LinkActionParams) => ActionReturn<LinkActionParams>;
|
|
8
9
|
/**
|
|
9
10
|
* Factory function that captures router context during component initialization.
|
|
10
11
|
* Must be called during component init (not inside event handlers or effects).
|
|
@@ -23,4 +24,5 @@ export interface LinkActionParams {
|
|
|
23
24
|
* <a use:link={{ name: 'users', params: { id: '123' } }}>User Profile</a>
|
|
24
25
|
* ```
|
|
25
26
|
*/
|
|
26
|
-
export declare function createLinkAction():
|
|
27
|
+
export declare function createLinkAction(): LinkAction;
|
|
28
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ROUTER_KEY, getContextOrThrow } from "../context";
|
|
2
|
+
import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
|
|
3
3
|
import { shouldNavigate, applyLinkA11y } from "../dom-utils/index.js";
|
|
4
4
|
/**
|
|
5
5
|
* Factory function that captures router context during component initialization.
|
|
@@ -20,30 +20,28 @@ import { shouldNavigate, applyLinkA11y } from "../dom-utils/index.js";
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
export function createLinkAction() {
|
|
23
|
-
const router =
|
|
24
|
-
if (!router) {
|
|
25
|
-
throw new Error("createLinkAction must be called inside a RouterProvider");
|
|
26
|
-
}
|
|
23
|
+
const router = getContextOrThrow(ROUTER_KEY, "createLinkAction");
|
|
27
24
|
return function link(node, params) {
|
|
28
25
|
let currentParams = params;
|
|
29
26
|
applyLinkA11y(node);
|
|
27
|
+
function navigate() {
|
|
28
|
+
router
|
|
29
|
+
.navigate(currentParams.name, currentParams.params ?? EMPTY_PARAMS, currentParams.options ?? EMPTY_OPTIONS)
|
|
30
|
+
.catch(NOOP);
|
|
31
|
+
}
|
|
30
32
|
function handleClick(evt) {
|
|
31
33
|
if (!shouldNavigate(evt))
|
|
32
34
|
return;
|
|
35
|
+
if (node instanceof HTMLAnchorElement &&
|
|
36
|
+
node.getAttribute("target") === "_blank") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
33
39
|
evt.preventDefault();
|
|
34
|
-
|
|
35
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
36
|
-
router
|
|
37
|
-
.navigate(currentParams.name, currentParams.params ?? {}, currentParams.options ?? {})
|
|
38
|
-
.catch(() => { });
|
|
40
|
+
navigate();
|
|
39
41
|
}
|
|
40
42
|
function handleKeyDown(evt) {
|
|
41
43
|
if (evt.key === "Enter" && !(node instanceof HTMLButtonElement)) {
|
|
42
|
-
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
44
|
-
router
|
|
45
|
-
.navigate(currentParams.name, currentParams.params ?? {}, currentParams.options ?? {})
|
|
46
|
-
.catch(() => { });
|
|
44
|
+
navigate();
|
|
47
45
|
}
|
|
48
46
|
}
|
|
49
47
|
node.addEventListener("click", handleClick);
|
|
@@ -9,26 +9,33 @@
|
|
|
9
9
|
fallback?: Component | undefined;
|
|
10
10
|
} = $props();
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
type LazyState =
|
|
13
|
+
| { status: "loading" }
|
|
14
|
+
| { status: "ready"; component: Component }
|
|
15
|
+
| { status: "error"; error: Error };
|
|
16
|
+
|
|
17
|
+
let state = $state<LazyState>({ status: "loading" });
|
|
15
18
|
|
|
16
19
|
$effect(() => {
|
|
17
|
-
|
|
18
|
-
error = null;
|
|
19
|
-
LoadedComponent = null;
|
|
20
|
+
state = { status: "loading" };
|
|
20
21
|
let active = true;
|
|
21
22
|
|
|
22
23
|
loader()
|
|
23
24
|
.then((module) => {
|
|
24
25
|
if (!active) return;
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
if (!module || typeof module.default === "undefined") {
|
|
27
|
+
throw new Error(
|
|
28
|
+
"[real-router] Lazy loader resolved without a `default` export.",
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
state = { status: "ready", component: module.default };
|
|
27
32
|
})
|
|
28
|
-
.catch((err) => {
|
|
33
|
+
.catch((err: unknown) => {
|
|
29
34
|
if (!active) return;
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
state = {
|
|
36
|
+
status: "error",
|
|
37
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
38
|
+
};
|
|
32
39
|
});
|
|
33
40
|
|
|
34
41
|
return () => {
|
|
@@ -37,11 +44,12 @@
|
|
|
37
44
|
});
|
|
38
45
|
</script>
|
|
39
46
|
|
|
40
|
-
{#if loading && fallback}
|
|
47
|
+
{#if state.status === "loading" && fallback}
|
|
41
48
|
{@const Fallback = fallback}
|
|
42
49
|
<Fallback />
|
|
43
|
-
{:else if error}
|
|
44
|
-
<p>Error loading component: {error.message}</p>
|
|
45
|
-
{:else if
|
|
50
|
+
{:else if state.status === "error"}
|
|
51
|
+
<p>Error loading component: {state.error.message}</p>
|
|
52
|
+
{:else if state.status === "ready"}
|
|
53
|
+
{@const LoadedComponent = state.component}
|
|
46
54
|
<LoadedComponent />
|
|
47
55
|
{/if}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useIsActiveRoute } from "../composables/useIsActiveRoute.svelte";
|
|
3
3
|
import { useRouter } from "../composables/useRouter.svelte";
|
|
4
|
+
import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
|
|
4
5
|
import {
|
|
5
6
|
shouldNavigate,
|
|
6
7
|
buildHref,
|
|
@@ -12,8 +13,8 @@
|
|
|
12
13
|
|
|
13
14
|
let {
|
|
14
15
|
routeName,
|
|
15
|
-
routeParams =
|
|
16
|
-
routeOptions =
|
|
16
|
+
routeParams = EMPTY_PARAMS,
|
|
17
|
+
routeOptions = EMPTY_OPTIONS,
|
|
17
18
|
class: className = undefined,
|
|
18
19
|
activeClassName = "active",
|
|
19
20
|
activeStrict = false,
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
evt.preventDefault();
|
|
67
|
-
router.navigate(routeName, routeParams, routeOptions).catch(
|
|
68
|
+
router.navigate(routeName, routeParams, routeOptions).catch(NOOP);
|
|
68
69
|
}
|
|
69
70
|
</script>
|
|
70
71
|
|
|
@@ -1,6 +1,26 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { startsWithSegment } from "@real-router/route-utils";
|
|
3
|
+
|
|
4
|
+
export function getActiveSegment(
|
|
5
|
+
routeName: string,
|
|
6
|
+
node: string,
|
|
7
|
+
snippets: Record<string, unknown>,
|
|
8
|
+
): string {
|
|
9
|
+
const prefix = node ? `${node}.` : "";
|
|
10
|
+
|
|
11
|
+
for (const segment in snippets) {
|
|
12
|
+
if (segment === "notFound") continue;
|
|
13
|
+
if (startsWithSegment(routeName, prefix + segment)) {
|
|
14
|
+
return segment;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
1
22
|
<script lang="ts">
|
|
2
23
|
import { UNKNOWN_ROUTE } from "@real-router/core";
|
|
3
|
-
import { startsWithSegment } from "@real-router/route-utils";
|
|
4
24
|
|
|
5
25
|
import { useRouteNode } from "../composables/useRouteNode.svelte";
|
|
6
26
|
|
|
@@ -17,29 +37,14 @@
|
|
|
17
37
|
} = $props();
|
|
18
38
|
|
|
19
39
|
const routeContext = useRouteNode(nodeName);
|
|
20
|
-
|
|
21
|
-
function getActiveSegment(
|
|
22
|
-
routeName: string,
|
|
23
|
-
node: string,
|
|
24
|
-
snippets: Record<string, unknown>,
|
|
25
|
-
): string {
|
|
26
|
-
for (const segment of Object.keys(snippets)) {
|
|
27
|
-
const fullSegmentName = node ? `${node}.${segment}` : segment;
|
|
28
|
-
|
|
29
|
-
if (startsWithSegment(routeName, fullSegmentName)) {
|
|
30
|
-
return segment;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return "";
|
|
35
|
-
}
|
|
36
40
|
</script>
|
|
37
41
|
|
|
38
42
|
{#if routeContext.route.current}
|
|
39
43
|
{@const route = routeContext.route.current}
|
|
40
44
|
{@const segment = getActiveSegment(route.name, nodeName, segmentSnippets)}
|
|
41
|
-
{#if segment
|
|
42
|
-
{@
|
|
45
|
+
{#if segment}
|
|
46
|
+
{@const snippet = segmentSnippets[segment] as Snippet}
|
|
47
|
+
{@render snippet()}
|
|
43
48
|
{:else if route.name === UNKNOWN_ROUTE && notFound}
|
|
44
49
|
{@render notFound()}
|
|
45
50
|
{/if}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { createDismissableError } from "@real-router/sources";
|
|
2
3
|
import { untrack } from "svelte";
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
+
import { useRouter } from "../composables/useRouter.svelte";
|
|
6
|
+
import { createReactiveSource } from "../createReactiveSource.svelte";
|
|
5
7
|
|
|
6
8
|
import type { RouterError, State } from "@real-router/core";
|
|
7
9
|
import type { Snippet } from "svelte";
|
|
@@ -18,30 +20,28 @@
|
|
|
18
20
|
|
|
19
21
|
let { children, fallback, onError }: Props = $props();
|
|
20
22
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const visibleError = $derived(
|
|
25
|
-
snapshot.current.version > dismissedVersion
|
|
26
|
-
? snapshot.current.error
|
|
27
|
-
: null,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
function resetError(): void {
|
|
31
|
-
dismissedVersion = snapshot.current.version;
|
|
32
|
-
}
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
const snapshot = createReactiveSource(createDismissableError(router));
|
|
33
25
|
|
|
34
26
|
$effect(() => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
const snap = snapshot.current;
|
|
28
|
+
if (!snap.error) return;
|
|
29
|
+
|
|
30
|
+
const { error, toRoute, fromRoute } = snap;
|
|
31
|
+
untrack(() => {
|
|
32
|
+
try {
|
|
38
33
|
onError?.(error, toRoute, fromRoute);
|
|
39
|
-
})
|
|
40
|
-
|
|
34
|
+
} catch (callbackError) {
|
|
35
|
+
console.error(
|
|
36
|
+
"[real-router] RouterErrorBoundary onError handler threw:",
|
|
37
|
+
callbackError,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
41
|
});
|
|
42
42
|
</script>
|
|
43
43
|
|
|
44
44
|
{@render children?.()}
|
|
45
|
-
{#if
|
|
46
|
-
{@render fallback(
|
|
45
|
+
{#if snapshot.current.error}
|
|
46
|
+
{@render fallback(snapshot.current.error, snapshot.current.resetError)}
|
|
47
47
|
{/if}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Params } from "@real-router/core";
|
|
2
|
-
export declare function useIsActiveRoute(routeName: string, params
|
|
2
|
+
export declare function useIsActiveRoute(routeName: string, params: Params | undefined, strict: boolean, ignoreQueryParams: boolean): {
|
|
3
3
|
readonly current: boolean;
|
|
4
4
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createActiveRouteSource } from "@real-router/sources";
|
|
2
2
|
import { createReactiveSource } from "../createReactiveSource.svelte";
|
|
3
3
|
import { useRouter } from "./useRouter.svelte";
|
|
4
|
-
export function useIsActiveRoute(routeName, params, strict
|
|
4
|
+
export function useIsActiveRoute(routeName, params, strict, ignoreQueryParams) {
|
|
5
5
|
const router = useRouter();
|
|
6
6
|
const source = createActiveRouteSource(router, routeName, params, {
|
|
7
7
|
strict,
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export const useNavigator = () => {
|
|
4
|
-
const navigator = getContext(NAVIGATOR_KEY);
|
|
5
|
-
if (!navigator) {
|
|
6
|
-
throw new Error("useNavigator must be used within a RouterProvider");
|
|
7
|
-
}
|
|
8
|
-
return navigator;
|
|
9
|
-
};
|
|
1
|
+
import { NAVIGATOR_KEY, getContextOrThrow } from "../context";
|
|
2
|
+
export const useNavigator = () => getContextOrThrow(NAVIGATOR_KEY, "useNavigator");
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export const useRoute = () => {
|
|
4
|
-
const routeContext = getContext(ROUTE_KEY);
|
|
5
|
-
if (!routeContext) {
|
|
6
|
-
throw new Error("useRoute must be used within a RouterProvider");
|
|
7
|
-
}
|
|
8
|
-
return routeContext;
|
|
9
|
-
};
|
|
1
|
+
import { ROUTE_KEY, getContextOrThrow } from "../context";
|
|
2
|
+
export const useRoute = () => getContextOrThrow(ROUTE_KEY, "useRoute");
|
|
@@ -1,27 +1,12 @@
|
|
|
1
1
|
import { getNavigator } from "@real-router/core";
|
|
2
2
|
import { createRouteNodeSource } from "@real-router/sources";
|
|
3
3
|
import { createReactiveSource } from "../createReactiveSource.svelte";
|
|
4
|
+
import { createRouteContext } from "../createRouteContext.svelte";
|
|
4
5
|
import { useRouter } from "./useRouter.svelte";
|
|
5
6
|
export function useRouteNode(nodeName) {
|
|
6
7
|
const router = useRouter();
|
|
7
8
|
const navigator = getNavigator(router);
|
|
8
9
|
const source = createRouteNodeSource(router, nodeName);
|
|
9
10
|
const reactive = createReactiveSource(source);
|
|
10
|
-
return
|
|
11
|
-
navigator,
|
|
12
|
-
get route() {
|
|
13
|
-
return {
|
|
14
|
-
get current() {
|
|
15
|
-
return reactive.current.route;
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
get previousRoute() {
|
|
20
|
-
return {
|
|
21
|
-
get current() {
|
|
22
|
-
return reactive.current.previousRoute;
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
};
|
|
11
|
+
return createRouteContext(navigator, reactive);
|
|
27
12
|
}
|
|
@@ -1,9 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export const useRouter = () => {
|
|
4
|
-
const router = getContext(ROUTER_KEY);
|
|
5
|
-
if (!router) {
|
|
6
|
-
throw new Error("useRouter must be used within a RouterProvider");
|
|
7
|
-
}
|
|
8
|
-
return router;
|
|
9
|
-
};
|
|
1
|
+
import { ROUTER_KEY, getContextOrThrow } from "../context";
|
|
2
|
+
export const useRouter = () => getContextOrThrow(ROUTER_KEY, "useRouter");
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getTransitionSource } from "@real-router/sources";
|
|
2
2
|
import { createReactiveSource } from "../createReactiveSource.svelte";
|
|
3
3
|
import { useRouter } from "./useRouter.svelte";
|
|
4
4
|
export function useRouterTransition() {
|
|
5
5
|
const router = useRouter();
|
|
6
|
-
const source =
|
|
6
|
+
const source = getTransitionSource(router);
|
|
7
7
|
return createReactiveSource(source);
|
|
8
8
|
}
|
package/dist/context.d.ts
CHANGED
package/dist/context.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
import { getContext } from "svelte";
|
|
1
2
|
export const ROUTER_KEY = "real-router:router";
|
|
2
3
|
export const NAVIGATOR_KEY = "real-router:navigator";
|
|
3
4
|
export const ROUTE_KEY = "real-router:route";
|
|
5
|
+
// The type parameter is used by the caller to narrow the return type.
|
|
6
|
+
// ESLint's no-unnecessary-type-parameters sees only a single textual use of T
|
|
7
|
+
// (the return type) — but each call site supplies a different T, so it is not
|
|
8
|
+
// unnecessary. Inline generic helpers are a standard pattern for typed context.
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
10
|
+
export function getContextOrThrow(key, consumerName) {
|
|
11
|
+
const value = getContext(key);
|
|
12
|
+
if (!value) {
|
|
13
|
+
throw new Error(`${consumerName} must be used within a RouterProvider`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Navigator, State } from "@real-router/core";
|
|
2
|
+
import type { RouteContext } from "./types";
|
|
3
|
+
export interface RouteSnapshot {
|
|
4
|
+
readonly route: State | undefined;
|
|
5
|
+
readonly previousRoute: State | undefined;
|
|
6
|
+
}
|
|
7
|
+
export declare function createRouteContext(navigator: Navigator, reactive: {
|
|
8
|
+
readonly current: RouteSnapshot;
|
|
9
|
+
}): RouteContext;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function createRouteContext(navigator, reactive) {
|
|
2
|
+
const route = {
|
|
3
|
+
get current() {
|
|
4
|
+
return reactive.current.route;
|
|
5
|
+
},
|
|
6
|
+
};
|
|
7
|
+
const previousRoute = {
|
|
8
|
+
get current() {
|
|
9
|
+
return reactive.current.previousRoute;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
return { navigator, route, previousRoute };
|
|
13
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { createRouteAnnouncer } from "./route-announcer.js";
|
|
2
|
-
export { shouldNavigate, buildHref, buildActiveClassName, applyLinkA11y, } from "./link-utils.js";
|
|
2
|
+
export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
|
|
3
3
|
export type { RouteAnnouncerOptions } from "./route-announcer.js";
|
package/dist/dom-utils/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createRouteAnnouncer } from "./route-announcer.js";
|
|
2
|
-
export { shouldNavigate, buildHref, buildActiveClassName, applyLinkA11y, } from "./link-utils.js";
|
|
2
|
+
export { shouldNavigate, buildHref, buildActiveClassName, shallowEqual, applyLinkA11y, } from "./link-utils.js";
|
|
@@ -2,4 +2,5 @@ import type { Router, Params } from "@real-router/core";
|
|
|
2
2
|
export declare function shouldNavigate(evt: MouseEvent): boolean;
|
|
3
3
|
export declare function buildHref(router: Router, routeName: string, routeParams: Params): string | undefined;
|
|
4
4
|
export declare function buildActiveClassName(isActive: boolean, activeClassName: string | undefined, baseClassName: string | undefined): string | undefined;
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function shallowEqual(prev: object | undefined, next: object | undefined): boolean;
|
|
6
|
+
export declare function applyLinkA11y(element: HTMLElement | null | undefined): void;
|
|
@@ -9,7 +9,10 @@ export function buildHref(router, routeName, routeParams) {
|
|
|
9
9
|
try {
|
|
10
10
|
const buildUrl = router.buildUrl;
|
|
11
11
|
if (buildUrl) {
|
|
12
|
-
|
|
12
|
+
const url = buildUrl(routeName, routeParams);
|
|
13
|
+
if (url !== undefined) {
|
|
14
|
+
return url;
|
|
15
|
+
}
|
|
13
16
|
}
|
|
14
17
|
return router.buildPath(routeName, routeParams);
|
|
15
18
|
}
|
|
@@ -18,23 +21,62 @@ export function buildHref(router, routeName, routeParams) {
|
|
|
18
21
|
return undefined;
|
|
19
22
|
}
|
|
20
23
|
}
|
|
24
|
+
function parseTokens(value) {
|
|
25
|
+
return value ? (value.match(/\S+/g) ?? []) : [];
|
|
26
|
+
}
|
|
21
27
|
export function buildActiveClassName(isActive, activeClassName, baseClassName) {
|
|
22
28
|
if (isActive && activeClassName) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const activeTokens = parseTokens(activeClassName);
|
|
30
|
+
if (activeTokens.length === 0) {
|
|
31
|
+
return baseClassName ?? undefined;
|
|
32
|
+
}
|
|
33
|
+
if (!baseClassName) {
|
|
34
|
+
return activeTokens.join(" ");
|
|
35
|
+
}
|
|
36
|
+
const baseTokens = parseTokens(baseClassName);
|
|
37
|
+
const seen = new Set(baseTokens);
|
|
38
|
+
for (const token of activeTokens) {
|
|
39
|
+
if (!seen.has(token)) {
|
|
40
|
+
seen.add(token);
|
|
41
|
+
baseTokens.push(token);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return baseTokens.join(" ");
|
|
26
45
|
}
|
|
27
46
|
return baseClassName ?? undefined;
|
|
28
47
|
}
|
|
48
|
+
export function shallowEqual(prev, next) {
|
|
49
|
+
if (Object.is(prev, next)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (!prev || !next) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const prevKeys = Object.keys(prev);
|
|
56
|
+
if (prevKeys.length !== Object.keys(next).length) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const prevRecord = prev;
|
|
60
|
+
const nextRecord = next;
|
|
61
|
+
for (const key of prevKeys) {
|
|
62
|
+
if (!Object.is(prevRecord[key], nextRecord[key])) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
29
68
|
export function applyLinkA11y(element) {
|
|
69
|
+
if (!element) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
30
72
|
if (element instanceof HTMLAnchorElement ||
|
|
31
73
|
element instanceof HTMLButtonElement) {
|
|
32
74
|
return;
|
|
33
75
|
}
|
|
34
|
-
if (!element.
|
|
76
|
+
if (!element.hasAttribute("role")) {
|
|
35
77
|
element.setAttribute("role", "link");
|
|
36
78
|
}
|
|
37
|
-
if (!element.
|
|
79
|
+
if (!element.hasAttribute("tabindex")) {
|
|
38
80
|
element.setAttribute("tabindex", "0");
|
|
39
81
|
}
|
|
40
82
|
}
|