@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
|
@@ -10,32 +10,57 @@ export function createRouteAnnouncer(router, options) {
|
|
|
10
10
|
let isReady = false;
|
|
11
11
|
let isDestroyed = false;
|
|
12
12
|
let lastAnnouncedText = "";
|
|
13
|
+
let pendingText = null;
|
|
13
14
|
let clearTimeoutId;
|
|
14
15
|
const announcer = getOrCreateAnnouncer();
|
|
16
|
+
const doAnnounce = (text, h1) => {
|
|
17
|
+
lastAnnouncedText = text;
|
|
18
|
+
clearTimeout(clearTimeoutId);
|
|
19
|
+
announcer.textContent = text;
|
|
20
|
+
clearTimeoutId = setTimeout(() => {
|
|
21
|
+
announcer.textContent = "";
|
|
22
|
+
lastAnnouncedText = "";
|
|
23
|
+
}, CLEAR_DELAY);
|
|
24
|
+
manageFocus(h1);
|
|
25
|
+
};
|
|
26
|
+
// Safari-ready delay: announcing before VoiceOver wires up the aria-live region
|
|
27
|
+
// causes the first announcement to be silently dropped. Wait SAFARI_READY_DELAY ms
|
|
28
|
+
// before marking the announcer "ready" — any navigation during that window is
|
|
29
|
+
// buffered in pendingText and flushed once the delay expires.
|
|
15
30
|
const safariTimeoutId = setTimeout(() => {
|
|
16
31
|
isReady = true;
|
|
32
|
+
if (pendingText !== null && !isDestroyed) {
|
|
33
|
+
const text = pendingText;
|
|
34
|
+
pendingText = null;
|
|
35
|
+
doAnnounce(text, document.querySelector("h1"));
|
|
36
|
+
}
|
|
17
37
|
}, SAFARI_READY_DELAY);
|
|
18
38
|
const unsubscribe = router.subscribe(({ route }) => {
|
|
19
39
|
if (isInitialNavigation) {
|
|
20
40
|
isInitialNavigation = false;
|
|
21
41
|
return;
|
|
22
42
|
}
|
|
43
|
+
// Double rAF: waits for two paint frames so the incoming route's DOM
|
|
44
|
+
// (including the new <h1>) is fully rendered before resolveText reads it.
|
|
45
|
+
// Single rAF fires before the new route's template has been attached,
|
|
46
|
+
// which would cause resolveText to pick up the OLD h1 or fall back to
|
|
47
|
+
// document.title / route.name prematurely.
|
|
23
48
|
requestAnimationFrame(() => {
|
|
24
49
|
requestAnimationFrame(() => {
|
|
25
50
|
if (isDestroyed) {
|
|
26
51
|
return;
|
|
27
52
|
}
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
manageFocus();
|
|
53
|
+
const h1 = document.querySelector("h1");
|
|
54
|
+
const text = resolveText(route, prefix, getCustomText, h1);
|
|
55
|
+
if (!text || text === lastAnnouncedText) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!isReady) {
|
|
59
|
+
// Defer announcement until Safari-ready window elapses (see safariTimeoutId).
|
|
60
|
+
pendingText = text;
|
|
61
|
+
return;
|
|
38
62
|
}
|
|
63
|
+
doAnnounce(text, h1);
|
|
39
64
|
});
|
|
40
65
|
});
|
|
41
66
|
});
|
|
@@ -65,11 +90,10 @@ function getOrCreateAnnouncer() {
|
|
|
65
90
|
function removeAnnouncer() {
|
|
66
91
|
document.querySelector(`[${ANNOUNCER_ATTR}]`)?.remove();
|
|
67
92
|
}
|
|
68
|
-
function resolveText(route, prefix, getCustomText) {
|
|
93
|
+
function resolveText(route, prefix, getCustomText, h1) {
|
|
69
94
|
if (getCustomText) {
|
|
70
95
|
return getCustomText(route);
|
|
71
96
|
}
|
|
72
|
-
const h1 = document.querySelector("h1");
|
|
73
97
|
const h1Text = h1?.textContent.trim() ?? "";
|
|
74
98
|
const routeName = route.name.startsWith(INTERNAL_ROUTE_PREFIX)
|
|
75
99
|
? ""
|
|
@@ -77,8 +101,7 @@ function resolveText(route, prefix, getCustomText) {
|
|
|
77
101
|
const rawText = h1Text || document.title || routeName || globalThis.location.pathname;
|
|
78
102
|
return `${prefix}${rawText}`;
|
|
79
103
|
}
|
|
80
|
-
function manageFocus() {
|
|
81
|
-
const h1 = document.querySelector("h1");
|
|
104
|
+
function manageFocus(h1) {
|
|
82
105
|
if (!h1) {
|
|
83
106
|
return;
|
|
84
107
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Svelte 5 integration for Real-Router",
|
|
6
6
|
"svelte": "./dist/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@real-router/core": "^0.48.0",
|
|
48
48
|
"@real-router/route-utils": "^0.2.1",
|
|
49
|
-
"@real-router/sources": "^0.
|
|
49
|
+
"@real-router/sources": "^0.6.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@sveltejs/package": "2.5.7",
|
|
@@ -58,17 +58,17 @@
|
|
|
58
58
|
"svelte": "5.54.0",
|
|
59
59
|
"svelte-check": "4.4.5",
|
|
60
60
|
"svelte-eslint-parser": "1.6.0",
|
|
61
|
-
"@real-router/browser-plugin": "^0.
|
|
61
|
+
"@real-router/browser-plugin": "^0.13.0"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"svelte": ">=5.7.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
-
"build": "svelte-package -i src -o dist",
|
|
68
67
|
"test": "vitest",
|
|
69
68
|
"test:properties": "vitest run --config vitest.config.properties.mts",
|
|
70
69
|
"test:stress": "vitest run --config vitest.config.stress.mts",
|
|
71
70
|
"type-check": "svelte-check --tsconfig ./tsconfig.json",
|
|
72
|
-
"lint": "eslint --cache src/ tests/ --fix --max-warnings 0"
|
|
71
|
+
"lint": "eslint --cache src/ tests/ --fix --max-warnings 0",
|
|
72
|
+
"bundle": "svelte-package -i src -o dist"
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -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()}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { getContext } from "svelte";
|
|
2
1
|
import type { ActionReturn } from "svelte/action";
|
|
3
2
|
import type { Router, Params, NavigationOptions } from "@real-router/core";
|
|
4
|
-
import { ROUTER_KEY } from "../context";
|
|
3
|
+
import { ROUTER_KEY, getContextOrThrow } from "../context";
|
|
4
|
+
import { EMPTY_OPTIONS, EMPTY_PARAMS, NOOP } from "../constants";
|
|
5
5
|
import { shouldNavigate, applyLinkA11y } from "../dom-utils/index.js";
|
|
6
6
|
|
|
7
7
|
export interface LinkActionParams {
|
|
@@ -10,6 +10,11 @@ export interface LinkActionParams {
|
|
|
10
10
|
options?: NavigationOptions;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
type LinkAction = (
|
|
14
|
+
node: HTMLElement,
|
|
15
|
+
params: LinkActionParams,
|
|
16
|
+
) => ActionReturn<LinkActionParams>;
|
|
17
|
+
|
|
13
18
|
/**
|
|
14
19
|
* Factory function that captures router context during component initialization.
|
|
15
20
|
* Must be called during component init (not inside event handlers or effects).
|
|
@@ -28,15 +33,8 @@ export interface LinkActionParams {
|
|
|
28
33
|
* <a use:link={{ name: 'users', params: { id: '123' } }}>User Profile</a>
|
|
29
34
|
* ```
|
|
30
35
|
*/
|
|
31
|
-
export function createLinkAction():
|
|
32
|
-
|
|
33
|
-
params: LinkActionParams,
|
|
34
|
-
) => ActionReturn<LinkActionParams> {
|
|
35
|
-
const router = getContext<Router | undefined>(ROUTER_KEY);
|
|
36
|
-
|
|
37
|
-
if (!router) {
|
|
38
|
-
throw new Error("createLinkAction must be called inside a RouterProvider");
|
|
39
|
-
}
|
|
36
|
+
export function createLinkAction(): LinkAction {
|
|
37
|
+
const router = getContextOrThrow<Router>(ROUTER_KEY, "createLinkAction");
|
|
40
38
|
|
|
41
39
|
return function link(
|
|
42
40
|
node: HTMLElement,
|
|
@@ -46,31 +44,31 @@ export function createLinkAction(): (
|
|
|
46
44
|
|
|
47
45
|
applyLinkA11y(node);
|
|
48
46
|
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
evt.preventDefault();
|
|
52
|
-
// router is guaranteed to exist due to check in factory
|
|
53
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
54
|
-
router!
|
|
47
|
+
function navigate() {
|
|
48
|
+
router
|
|
55
49
|
.navigate(
|
|
56
50
|
currentParams.name,
|
|
57
|
-
currentParams.params ??
|
|
58
|
-
currentParams.options ??
|
|
51
|
+
currentParams.params ?? EMPTY_PARAMS,
|
|
52
|
+
currentParams.options ?? EMPTY_OPTIONS,
|
|
59
53
|
)
|
|
60
|
-
.catch(
|
|
54
|
+
.catch(NOOP);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleClick(evt: MouseEvent) {
|
|
58
|
+
if (!shouldNavigate(evt)) return;
|
|
59
|
+
if (
|
|
60
|
+
node instanceof HTMLAnchorElement &&
|
|
61
|
+
node.getAttribute("target") === "_blank"
|
|
62
|
+
) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
evt.preventDefault();
|
|
66
|
+
navigate();
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
function handleKeyDown(evt: KeyboardEvent) {
|
|
64
70
|
if (evt.key === "Enter" && !(node instanceof HTMLButtonElement)) {
|
|
65
|
-
|
|
66
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
67
|
-
router!
|
|
68
|
-
.navigate(
|
|
69
|
-
currentParams.name,
|
|
70
|
-
currentParams.params ?? {},
|
|
71
|
-
currentParams.options ?? {},
|
|
72
|
-
)
|
|
73
|
-
.catch(() => {});
|
|
71
|
+
navigate();
|
|
74
72
|
}
|
|
75
73
|
}
|
|
76
74
|
|
|
@@ -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}
|
|
@@ -7,9 +7,9 @@ import type { Params } from "@real-router/core";
|
|
|
7
7
|
|
|
8
8
|
export function useIsActiveRoute(
|
|
9
9
|
routeName: string,
|
|
10
|
-
params
|
|
11
|
-
strict
|
|
12
|
-
ignoreQueryParams
|
|
10
|
+
params: Params | undefined,
|
|
11
|
+
strict: boolean,
|
|
12
|
+
ignoreQueryParams: boolean,
|
|
13
13
|
): { readonly current: boolean } {
|
|
14
14
|
const router = useRouter();
|
|
15
15
|
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { NAVIGATOR_KEY } from "../context";
|
|
1
|
+
import { NAVIGATOR_KEY, getContextOrThrow } from "../context";
|
|
4
2
|
|
|
5
3
|
import type { Navigator } from "@real-router/core";
|
|
6
4
|
|
|
7
|
-
export const useNavigator = (): Navigator =>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!navigator) {
|
|
11
|
-
throw new Error("useNavigator must be used within a RouterProvider");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return navigator;
|
|
15
|
-
};
|
|
5
|
+
export const useNavigator = (): Navigator =>
|
|
6
|
+
getContextOrThrow<Navigator>(NAVIGATOR_KEY, "useNavigator");
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { ROUTE_KEY } from "../context";
|
|
1
|
+
import { ROUTE_KEY, getContextOrThrow } from "../context";
|
|
4
2
|
|
|
5
3
|
import type { RouteContext } from "../types";
|
|
6
4
|
|
|
7
|
-
export const useRoute = (): RouteContext =>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!routeContext) {
|
|
11
|
-
throw new Error("useRoute must be used within a RouterProvider");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return routeContext;
|
|
15
|
-
};
|
|
5
|
+
export const useRoute = (): RouteContext =>
|
|
6
|
+
getContextOrThrow<RouteContext>(ROUTE_KEY, "useRoute");
|
|
@@ -2,6 +2,7 @@ import { getNavigator } from "@real-router/core";
|
|
|
2
2
|
import { createRouteNodeSource } from "@real-router/sources";
|
|
3
3
|
|
|
4
4
|
import { createReactiveSource } from "../createReactiveSource.svelte";
|
|
5
|
+
import { createRouteContext } from "../createRouteContext.svelte";
|
|
5
6
|
import { useRouter } from "./useRouter.svelte";
|
|
6
7
|
|
|
7
8
|
import type { RouteContext } from "../types";
|
|
@@ -13,21 +14,5 @@ export function useRouteNode(nodeName: string): RouteContext {
|
|
|
13
14
|
const source = createRouteNodeSource(router, nodeName);
|
|
14
15
|
const reactive = createReactiveSource(source);
|
|
15
16
|
|
|
16
|
-
return
|
|
17
|
-
navigator,
|
|
18
|
-
get route() {
|
|
19
|
-
return {
|
|
20
|
-
get current() {
|
|
21
|
-
return reactive.current.route;
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
},
|
|
25
|
-
get previousRoute() {
|
|
26
|
-
return {
|
|
27
|
-
get current() {
|
|
28
|
-
return reactive.current.previousRoute;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
};
|
|
17
|
+
return createRouteContext(navigator, reactive);
|
|
33
18
|
}
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { ROUTER_KEY } from "../context";
|
|
1
|
+
import { ROUTER_KEY, getContextOrThrow } from "../context";
|
|
4
2
|
|
|
5
3
|
import type { Router } from "@real-router/core";
|
|
6
4
|
|
|
7
|
-
export const useRouter = (): Router =>
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!router) {
|
|
11
|
-
throw new Error("useRouter must be used within a RouterProvider");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return router;
|
|
15
|
-
};
|
|
5
|
+
export const useRouter = (): Router =>
|
|
6
|
+
getContextOrThrow<Router>(ROUTER_KEY, "useRouter");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getTransitionSource } from "@real-router/sources";
|
|
2
2
|
|
|
3
3
|
import { createReactiveSource } from "../createReactiveSource.svelte";
|
|
4
4
|
import { useRouter } from "./useRouter.svelte";
|
|
@@ -9,8 +9,7 @@ export function useRouterTransition(): {
|
|
|
9
9
|
readonly current: RouterTransitionSnapshot;
|
|
10
10
|
} {
|
|
11
11
|
const router = useRouter();
|
|
12
|
-
|
|
13
|
-
const source = createTransitionSource(router);
|
|
12
|
+
const source = getTransitionSource(router);
|
|
14
13
|
|
|
15
14
|
return createReactiveSource(source);
|
|
16
15
|
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NavigationOptions, Params } from "@real-router/core";
|
|
2
|
+
|
|
3
|
+
export const EMPTY_PARAMS: Params = Object.freeze({}) as Params;
|
|
4
|
+
|
|
5
|
+
export const EMPTY_OPTIONS: NavigationOptions = Object.freeze(
|
|
6
|
+
{},
|
|
7
|
+
) as NavigationOptions;
|
|
8
|
+
|
|
9
|
+
export const NOOP = (): void => {};
|
package/src/context.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
+
import { getContext } from "svelte";
|
|
2
|
+
|
|
1
3
|
export const ROUTER_KEY = "real-router:router";
|
|
2
4
|
|
|
3
5
|
export const NAVIGATOR_KEY = "real-router:navigator";
|
|
4
6
|
|
|
5
7
|
export const ROUTE_KEY = "real-router:route";
|
|
8
|
+
|
|
9
|
+
// The type parameter is used by the caller to narrow the return type.
|
|
10
|
+
// ESLint's no-unnecessary-type-parameters sees only a single textual use of T
|
|
11
|
+
// (the return type) — but each call site supplies a different T, so it is not
|
|
12
|
+
// unnecessary. Inline generic helpers are a standard pattern for typed context.
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
|
|
14
|
+
export function getContextOrThrow<T>(key: string, consumerName: string): T {
|
|
15
|
+
const value = getContext<T | undefined>(key);
|
|
16
|
+
|
|
17
|
+
if (!value) {
|
|
18
|
+
throw new Error(`${consumerName} must be used within a RouterProvider`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Navigator, State } from "@real-router/core";
|
|
2
|
+
|
|
3
|
+
import type { RouteContext } from "./types";
|
|
4
|
+
|
|
5
|
+
export interface RouteSnapshot {
|
|
6
|
+
readonly route: State | undefined;
|
|
7
|
+
readonly previousRoute: State | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createRouteContext(
|
|
11
|
+
navigator: Navigator,
|
|
12
|
+
reactive: { readonly current: RouteSnapshot },
|
|
13
|
+
): RouteContext {
|
|
14
|
+
const route = {
|
|
15
|
+
get current(): State | undefined {
|
|
16
|
+
return reactive.current.route;
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const previousRoute = {
|
|
21
|
+
get current(): State | undefined {
|
|
22
|
+
return reactive.current.previousRoute;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return { navigator, route, previousRoute };
|
|
27
|
+
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { createErrorSource } from "@real-router/sources";
|
|
2
|
-
import { createReactiveSource } from "../createReactiveSource.svelte";
|
|
3
|
-
import { useRouter } from "./useRouter.svelte";
|
|
4
|
-
const cache = new WeakMap();
|
|
5
|
-
export function useRouterError() {
|
|
6
|
-
const router = useRouter();
|
|
7
|
-
let source = cache.get(router);
|
|
8
|
-
if (!source) {
|
|
9
|
-
source = createErrorSource(router);
|
|
10
|
-
cache.set(router, source);
|
|
11
|
-
}
|
|
12
|
-
return createReactiveSource(source);
|
|
13
|
-
}
|