@real-router/vue 0.4.1 → 0.5.0

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.
@@ -9,28 +9,100 @@ export interface LinkDirectiveValue {
9
9
  options?: NavigationOptions;
10
10
  }
11
11
 
12
- let _router: Router | null = null;
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
- export function setDirectiveRouter(router: Router): void {
15
- _router = router;
59
+ routerStack[routerStack.length - 1] = router;
16
60
  }
17
61
 
18
62
  export function getDirectiveRouter(): Router {
19
- if (!_router) {
20
- /* v8 ignore next 3 -- @preserve Defensive: router always initialized by RouterProvider */
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 _router;
71
+ return top;
72
+ }
73
+
74
+ interface Handlers {
75
+ click: (evt: MouseEvent) => void;
76
+ keydown: (evt: KeyboardEvent) => void;
27
77
  }
28
78
 
29
- const clickHandlers = new WeakMap<HTMLElement, (evt: MouseEvent) => void>();
30
- const keydownHandlers = new WeakMap<
31
- HTMLElement,
32
- (evt: KeyboardEvent) => void
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 handleClick = createClickHandler(router, value);
71
- const handleKeyDown = createKeydownHandler(router, value, element);
142
+ const click = createClickHandler(router, value);
143
+ const keydown = createKeydownHandler(router, value, element);
72
144
 
73
- element.addEventListener("click", handleClick);
74
- element.addEventListener("keydown", handleKeyDown);
145
+ element.addEventListener("click", click);
146
+ element.addEventListener("keydown", keydown);
75
147
 
76
- clickHandlers.set(element, handleClick);
77
- keydownHandlers.set(element, handleKeyDown);
148
+ handlers.set(element, { click, keydown });
78
149
  }
79
150
 
80
151
  function detachHandlers(element: HTMLElement): void {
81
- const clickHandler = clickHandlers.get(element);
82
- const keydownHandler = keydownHandlers.get(element);
152
+ const entry = handlers.get(element);
83
153
 
84
- if (clickHandler) {
85
- element.removeEventListener("click", clickHandler);
86
- }
87
- if (keydownHandler) {
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 { ShallowRef } from "vue";
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: ShallowRef<State | undefined>;
12
- previousRoute: ShallowRef<State | undefined>;
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> {