@real-router/vue 0.4.1 → 0.5.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 +26 -14
- package/dist/cjs/index.d.ts +9 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.mts +9 -3
- package/dist/esm/index.d.mts.map +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/index.mjs.map +1 -1
- package/package.json +5 -5
- package/src/RouterProvider.ts +33 -40
- package/src/components/Link.ts +90 -12
- package/src/components/RouteView/RouteView.ts +28 -6
- package/src/components/RouterErrorBoundary.ts +8 -16
- package/src/composables/useRouteNode.ts +8 -15
- package/src/composables/useRouterTransition.ts +2 -3
- package/src/createRouterPlugin.ts +17 -22
- package/src/directives/vLink.ts +102 -27
- package/src/setupRouteProvision.ts +42 -0
- package/src/types.ts +9 -3
- package/src/composables/useRouterError.ts +0 -23
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getTransitionSource } from "@real-router/sources";
|
|
2
2
|
|
|
3
3
|
import { useRefFromSource } from "../useRefFromSource";
|
|
4
4
|
import { useRouter } from "./useRouter";
|
|
@@ -8,8 +8,7 @@ import type { ShallowRef } from "vue";
|
|
|
8
8
|
|
|
9
9
|
export function useRouterTransition(): ShallowRef<RouterTransitionSnapshot> {
|
|
10
10
|
const router = useRouter();
|
|
11
|
-
|
|
12
|
-
const source = createTransitionSource(router);
|
|
11
|
+
const source = getTransitionSource(router);
|
|
13
12
|
|
|
14
13
|
return useRefFromSource(source);
|
|
15
14
|
}
|
|
@@ -1,32 +1,27 @@
|
|
|
1
|
-
import { getNavigator } from "@real-router/core";
|
|
2
|
-
import { createRouteSource } from "@real-router/sources";
|
|
3
|
-
import { shallowRef } from "vue";
|
|
4
|
-
|
|
5
1
|
import { NavigatorKey, RouteKey, RouterKey } from "./context";
|
|
6
|
-
import {
|
|
2
|
+
import { pushDirectiveRouter } from "./directives/vLink";
|
|
3
|
+
import { setupRouteProvision } from "./setupRouteProvision";
|
|
7
4
|
|
|
8
5
|
import type { Router } from "@real-router/core";
|
|
9
|
-
import type { Plugin } from "vue";
|
|
6
|
+
import type { App, Plugin } from "vue";
|
|
10
7
|
|
|
11
8
|
export function createRouterPlugin(router: Router): Plugin<[]> {
|
|
12
9
|
return {
|
|
13
10
|
install(app): void {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
previousRoute.value = snapshot.previousRoute;
|
|
29
|
-
});
|
|
11
|
+
const releaseDirective = pushDirectiveRouter(router);
|
|
12
|
+
|
|
13
|
+
const { navigator, route, previousRoute, unsubscribe } =
|
|
14
|
+
setupRouteProvision(router);
|
|
15
|
+
|
|
16
|
+
// Vue 3.5+ exposes app.onUnmount for plugin cleanup.
|
|
17
|
+
// On older versions (3.3–3.4), the subscription is cleaned up
|
|
18
|
+
// when the router is garbage-collected (same as vue-router).
|
|
19
|
+
if ("onUnmount" in app) {
|
|
20
|
+
(app as App & { onUnmount: (fn: () => void) => void }).onUnmount(() => {
|
|
21
|
+
releaseDirective();
|
|
22
|
+
unsubscribe();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
30
25
|
|
|
31
26
|
app.provide(RouterKey, router);
|
|
32
27
|
app.provide(NavigatorKey, navigator);
|
package/src/directives/vLink.ts
CHANGED
|
@@ -9,28 +9,100 @@ export interface LinkDirectiveValue {
|
|
|
9
9
|
options?: NavigationOptions;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Router stack for nested RouterProviders. The active router is the top of
|
|
14
|
+
* the stack. RouterProvider pushes its router on mount and pops it on unmount,
|
|
15
|
+
* which preserves the parent context when an inner provider tears down.
|
|
16
|
+
*
|
|
17
|
+
* Without the stack, an unmounted child provider would leave the directive
|
|
18
|
+
* pointing at a disposed router, and v-link in the still-mounted parent would
|
|
19
|
+
* navigate via the wrong (or torn-down) instance.
|
|
20
|
+
*/
|
|
21
|
+
const routerStack: Router[] = [];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Pushes a router onto the active stack. Returns a release function that
|
|
25
|
+
* removes that exact router from the stack regardless of position — safe
|
|
26
|
+
* across out-of-order provider unmount sequences.
|
|
27
|
+
*
|
|
28
|
+
* @internal Used by RouterProvider during setup/teardown.
|
|
29
|
+
*/
|
|
30
|
+
export function pushDirectiveRouter(router: Router): () => void {
|
|
31
|
+
routerStack.push(router);
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
const idx = routerStack.lastIndexOf(router);
|
|
35
|
+
|
|
36
|
+
if (idx !== -1) {
|
|
37
|
+
routerStack.splice(idx, 1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Backwards-compatible alias. Replaces the active router unconditionally and
|
|
44
|
+
* does NOT participate in the stack — use {@link pushDirectiveRouter} from
|
|
45
|
+
* provider code instead. Kept for tests and direct callers.
|
|
46
|
+
*/
|
|
47
|
+
export function setDirectiveRouter(router: Router | null): void {
|
|
48
|
+
if (router === null) {
|
|
49
|
+
routerStack.length = 0;
|
|
50
|
+
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (routerStack.length === 0) {
|
|
54
|
+
routerStack.push(router);
|
|
55
|
+
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
13
58
|
|
|
14
|
-
|
|
15
|
-
_router = router;
|
|
59
|
+
routerStack[routerStack.length - 1] = router;
|
|
16
60
|
}
|
|
17
61
|
|
|
18
62
|
export function getDirectiveRouter(): Router {
|
|
19
|
-
|
|
20
|
-
|
|
63
|
+
const top = routerStack.at(-1);
|
|
64
|
+
|
|
65
|
+
if (!top) {
|
|
21
66
|
throw new Error(
|
|
22
67
|
"v-link directive requires a RouterProvider ancestor. Make sure RouterProvider is mounted.",
|
|
23
68
|
);
|
|
24
69
|
}
|
|
25
70
|
|
|
26
|
-
return
|
|
71
|
+
return top;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface Handlers {
|
|
75
|
+
click: (evt: MouseEvent) => void;
|
|
76
|
+
keydown: (evt: KeyboardEvent) => void;
|
|
27
77
|
}
|
|
28
78
|
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
79
|
+
// Single WeakMap halves per-element bookkeeping vs two parallel maps.
|
|
80
|
+
const handlers = new WeakMap<HTMLElement, Handlers>();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Validates a directive binding value before attaching handlers.
|
|
84
|
+
* Returns false (and warns once per call) when the value is missing or
|
|
85
|
+
* has no `name` — silently doing nothing is preferable to a runtime crash
|
|
86
|
+
* inside a click handler.
|
|
87
|
+
*/
|
|
88
|
+
function isValidBinding(value: unknown): value is LinkDirectiveValue {
|
|
89
|
+
if (value === null || value === undefined) {
|
|
90
|
+
console.error(
|
|
91
|
+
"[real-router] v-link directive received null/undefined value. The element will not be wired for navigation.",
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (typeof (value as { name?: unknown }).name !== "string") {
|
|
97
|
+
console.error(
|
|
98
|
+
"[real-router] v-link directive value is missing a string `name` field. The element will not be wired for navigation.",
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
34
106
|
|
|
35
107
|
function createClickHandler(
|
|
36
108
|
router: Router,
|
|
@@ -67,29 +139,23 @@ function attachHandlers(
|
|
|
67
139
|
router: Router,
|
|
68
140
|
value: LinkDirectiveValue,
|
|
69
141
|
): void {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
142
|
+
const click = createClickHandler(router, value);
|
|
143
|
+
const keydown = createKeydownHandler(router, value, element);
|
|
72
144
|
|
|
73
|
-
element.addEventListener("click",
|
|
74
|
-
element.addEventListener("keydown",
|
|
145
|
+
element.addEventListener("click", click);
|
|
146
|
+
element.addEventListener("keydown", keydown);
|
|
75
147
|
|
|
76
|
-
|
|
77
|
-
keydownHandlers.set(element, handleKeyDown);
|
|
148
|
+
handlers.set(element, { click, keydown });
|
|
78
149
|
}
|
|
79
150
|
|
|
80
151
|
function detachHandlers(element: HTMLElement): void {
|
|
81
|
-
const
|
|
82
|
-
const keydownHandler = keydownHandlers.get(element);
|
|
152
|
+
const entry = handlers.get(element);
|
|
83
153
|
|
|
84
|
-
if (
|
|
85
|
-
element.removeEventListener("click",
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
element.removeEventListener("keydown", keydownHandler);
|
|
154
|
+
if (entry) {
|
|
155
|
+
element.removeEventListener("click", entry.click);
|
|
156
|
+
element.removeEventListener("keydown", entry.keydown);
|
|
157
|
+
handlers.delete(element);
|
|
89
158
|
}
|
|
90
|
-
|
|
91
|
-
clickHandlers.delete(element);
|
|
92
|
-
keydownHandlers.delete(element);
|
|
93
159
|
}
|
|
94
160
|
|
|
95
161
|
export const vLink: Directive<HTMLElement, LinkDirectiveValue> = {
|
|
@@ -100,6 +166,10 @@ export const vLink: Directive<HTMLElement, LinkDirectiveValue> = {
|
|
|
100
166
|
|
|
101
167
|
element.style.cursor = "pointer";
|
|
102
168
|
|
|
169
|
+
if (!isValidBinding(binding.value)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
103
173
|
attachHandlers(element, router, binding.value);
|
|
104
174
|
},
|
|
105
175
|
|
|
@@ -107,6 +177,11 @@ export const vLink: Directive<HTMLElement, LinkDirectiveValue> = {
|
|
|
107
177
|
const router = getDirectiveRouter();
|
|
108
178
|
|
|
109
179
|
detachHandlers(element);
|
|
180
|
+
|
|
181
|
+
if (!isValidBinding(binding.value)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
110
185
|
attachHandlers(element, router, binding.value);
|
|
111
186
|
},
|
|
112
187
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// packages/vue/src/setupRouteProvision.ts
|
|
2
|
+
|
|
3
|
+
import { getNavigator } from "@real-router/core";
|
|
4
|
+
import { createRouteSource } from "@real-router/sources";
|
|
5
|
+
import { shallowRef } from "vue";
|
|
6
|
+
|
|
7
|
+
import type { Router, Navigator, State } from "@real-router/core";
|
|
8
|
+
import type { ShallowRef } from "vue";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Shared setup for `RouterProvider` (component-scoped) and
|
|
12
|
+
* `createRouterPlugin` (app-scoped). Builds the reactive route refs +
|
|
13
|
+
* subscription bookkeeping in one place; callers wire the result into their
|
|
14
|
+
* provide/inject mechanism and own teardown lifecycle.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/
|
|
18
|
+
export interface RouteProvision {
|
|
19
|
+
navigator: Navigator;
|
|
20
|
+
route: ShallowRef<State | undefined>;
|
|
21
|
+
previousRoute: ShallowRef<State | undefined>;
|
|
22
|
+
/** Call when the owning scope tears down to release the router subscription. */
|
|
23
|
+
unsubscribe: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setupRouteProvision(router: Router): RouteProvision {
|
|
27
|
+
const navigator = getNavigator(router);
|
|
28
|
+
const source = createRouteSource(router);
|
|
29
|
+
const initial = source.getSnapshot();
|
|
30
|
+
|
|
31
|
+
const route = shallowRef<State | undefined>(initial.route);
|
|
32
|
+
const previousRoute = shallowRef<State | undefined>(initial.previousRoute);
|
|
33
|
+
|
|
34
|
+
const unsubscribe = source.subscribe(() => {
|
|
35
|
+
const snapshot = source.getSnapshot();
|
|
36
|
+
|
|
37
|
+
route.value = snapshot.route;
|
|
38
|
+
previousRoute.value = snapshot.previousRoute;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return { navigator, route, previousRoute, unsubscribe };
|
|
42
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -4,12 +4,18 @@ import type {
|
|
|
4
4
|
Navigator,
|
|
5
5
|
State,
|
|
6
6
|
} from "@real-router/core";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Ref } from "vue";
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* `route`/`previousRoute` are read-only reactive references. They may be
|
|
11
|
+
* implemented as `shallowRef` or `computed` depending on the composable
|
|
12
|
+
* (`useRoute` mirrors via `shallowRef`, `useRouteNode` derives via `computed`),
|
|
13
|
+
* but consumers only need `.value` read access — typed as `Readonly<Ref<…>>`.
|
|
14
|
+
*/
|
|
9
15
|
export interface RouteContext {
|
|
10
16
|
navigator: Navigator;
|
|
11
|
-
route:
|
|
12
|
-
previousRoute:
|
|
17
|
+
route: Readonly<Ref<State | undefined>>;
|
|
18
|
+
previousRoute: Readonly<Ref<State | undefined>>;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface LinkProps<P extends Params = Params> {
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createErrorSource } from "@real-router/sources";
|
|
2
|
-
|
|
3
|
-
import { useRefFromSource } from "../useRefFromSource";
|
|
4
|
-
import { useRouter } from "./useRouter";
|
|
5
|
-
|
|
6
|
-
import type { Router } from "@real-router/core";
|
|
7
|
-
import type { RouterErrorSnapshot, RouterSource } from "@real-router/sources";
|
|
8
|
-
import type { ShallowRef } from "vue";
|
|
9
|
-
|
|
10
|
-
const cache = new WeakMap<Router, RouterSource<RouterErrorSnapshot>>();
|
|
11
|
-
|
|
12
|
-
export function useRouterError(): ShallowRef<RouterErrorSnapshot> {
|
|
13
|
-
const router = useRouter();
|
|
14
|
-
|
|
15
|
-
let source = cache.get(router);
|
|
16
|
-
|
|
17
|
-
if (!source) {
|
|
18
|
-
source = createErrorSource(router);
|
|
19
|
-
cache.set(router, source);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return useRefFromSource(source);
|
|
23
|
-
}
|