@real-router/vue 0.15.2 → 0.15.4

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.
Files changed (39) hide show
  1. package/dist/cjs/index.js +1 -1
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/esm/index.mjs +1 -1
  4. package/dist/esm/index.mjs.map +1 -1
  5. package/package.json +7 -8
  6. package/src/RouterProvider.ts +0 -162
  7. package/src/components/Await.ts +0 -47
  8. package/src/components/ClientOnly.ts +0 -16
  9. package/src/components/HttpStatusCode.ts +0 -74
  10. package/src/components/HttpStatusProvider.ts +0 -22
  11. package/src/components/Link.ts +0 -226
  12. package/src/components/RouteView/RouteView.ts +0 -233
  13. package/src/components/RouteView/components.ts +0 -53
  14. package/src/components/RouteView/helpers.ts +0 -207
  15. package/src/components/RouteView/index.ts +0 -8
  16. package/src/components/RouteView/types.ts +0 -20
  17. package/src/components/RouterErrorBoundary.ts +0 -61
  18. package/src/components/ServerOnly.ts +0 -16
  19. package/src/components/Streamed.ts +0 -31
  20. package/src/composables/useDeferred.ts +0 -37
  21. package/src/composables/useIsActiveRoute.ts +0 -61
  22. package/src/composables/useNavigator.ts +0 -15
  23. package/src/composables/useRoute.ts +0 -34
  24. package/src/composables/useRouteEnter.ts +0 -120
  25. package/src/composables/useRouteExit.ts +0 -116
  26. package/src/composables/useRouteNode.ts +0 -31
  27. package/src/composables/useRouteUtils.ts +0 -12
  28. package/src/composables/useRouter.ts +0 -15
  29. package/src/composables/useRouterTransition.ts +0 -14
  30. package/src/constants.ts +0 -9
  31. package/src/context.ts +0 -13
  32. package/src/createRouterPlugin.ts +0 -31
  33. package/src/directives/vLink.ts +0 -208
  34. package/src/index.ts +0 -64
  35. package/src/setupRouteProvision.ts +0 -42
  36. package/src/ssr.ts +0 -39
  37. package/src/types.ts +0 -40
  38. package/src/useRefFromSource.ts +0 -16
  39. package/src/utils/createHttpStatusSink.ts +0 -31
@@ -1,61 +0,0 @@
1
- import { createActiveRouteSource } from "@real-router/sources";
2
-
3
- import { useRefFromSource } from "../useRefFromSource";
4
- import { useRouter } from "./useRouter";
5
-
6
- import type { Params } from "@real-router/core";
7
- import type { ShallowRef } from "vue";
8
-
9
- /**
10
- * Options object for `useIsActiveRoute`. Replaces the previous trailing
11
- * positional booleans (`strict`, `ignoreQueryParams`) — positional flags at
12
- * call sites read as magic numbers and the order was easy to swap silently.
13
- *
14
- * The composable is `@internal` (consumed by `<Link>` and tests only), so
15
- * the signature changes without a deprecation cycle.
16
- */
17
- export interface UseIsActiveRouteOptions {
18
- /**
19
- * Match the route name exactly (no descendant match). Default: `false`.
20
- */
21
- strict?: boolean;
22
- /**
23
- * Ignore query params when comparing the active route. Default: `true`.
24
- */
25
- ignoreQueryParams?: boolean;
26
- /**
27
- * Hash-aware active state (#532) — when provided, the route is active only
28
- * if `state.context.url.hash` equals this value. Default: `undefined`
29
- * (hash is ignored).
30
- */
31
- hash?: string;
32
- }
33
-
34
- /**
35
- * @internal Consumed by `<Link>` via `createActiveRouteSource`. Not exported
36
- * from `@real-router/vue`.
37
- */
38
- export function useIsActiveRoute(
39
- routeName: string,
40
- params?: Params,
41
- options?: UseIsActiveRouteOptions,
42
- ): ShallowRef<boolean> {
43
- const router = useRouter();
44
- const strict = options?.strict ?? false;
45
- const ignoreQueryParams = options?.ignoreQueryParams ?? true;
46
- const hash = options?.hash;
47
-
48
- // The `hash` argument (#532) participates in the cache key when defined.
49
- // exactOptionalPropertyTypes forbids `{ hash: undefined }` literally — we
50
- // conditionally include the key only when a value is provided.
51
- const source = createActiveRouteSource(
52
- router,
53
- routeName,
54
- params,
55
- hash === undefined
56
- ? { strict, ignoreQueryParams }
57
- : { strict, ignoreQueryParams, hash },
58
- );
59
-
60
- return useRefFromSource(source);
61
- }
@@ -1,15 +0,0 @@
1
- import { inject } from "vue";
2
-
3
- import { NavigatorKey } from "../context";
4
-
5
- import type { Navigator } from "@real-router/core";
6
-
7
- export const useNavigator = (): Navigator => {
8
- const navigator = inject(NavigatorKey);
9
-
10
- if (!navigator) {
11
- throw new Error("useNavigator must be used within a RouterProvider");
12
- }
13
-
14
- return navigator;
15
- };
@@ -1,34 +0,0 @@
1
- import { inject } from "vue";
2
-
3
- import { RouteKey } from "../context";
4
-
5
- import type { RouteContext } from "../types";
6
- import type { Params, State } from "@real-router/core";
7
- import type { Ref } from "vue";
8
-
9
- /**
10
- * Return shape for `useRoute()` — `RouteContext<P>` with `route` narrowed
11
- * to the non-nullable variant. The composable throws when `route.value`
12
- * would be `undefined`, so consumers can read `.value.params.x` without a
13
- * nullable guard. Extracted from inline duplication at two call sites.
14
- */
15
- export type UseRouteReturn<P extends Params = Params> = Omit<
16
- RouteContext<P>,
17
- "route"
18
- > & { route: Readonly<Ref<State<P>>> };
19
-
20
- export const useRoute = <P extends Params = Params>(): UseRouteReturn<P> => {
21
- const routeContext = inject(RouteKey);
22
-
23
- if (!routeContext) {
24
- throw new Error("useRoute must be used within a RouterProvider");
25
- }
26
-
27
- if (!routeContext.route.value) {
28
- throw new Error(
29
- "useRoute called with no active route. Did you forget to await router.start() before rendering, or is the router stopped/disposed?",
30
- );
31
- }
32
-
33
- return routeContext as UseRouteReturn<P>;
34
- };
@@ -1,120 +0,0 @@
1
- import { watch } from "vue";
2
-
3
- import { useRoute } from "./useRoute";
4
-
5
- import type { State } from "@real-router/core";
6
-
7
- export interface RouteEnterContext {
8
- /** The route that was just activated. */
9
- route: State;
10
- /** The route that was active immediately before this navigation. */
11
- previousRoute: State;
12
- }
13
-
14
- export type RouteEnterHandler = (context: RouteEnterContext) => void;
15
-
16
- export interface UseRouteEnterOptions {
17
- /**
18
- * Skip the handler when `route.name === previousRoute.name`
19
- * (sort/filter/query-only navigations on the same route). Default:
20
- * `true`. Symmetric with `useRouteExit`'s same-name option.
21
- */
22
- skipSameRoute?: boolean;
23
- }
24
-
25
- /**
26
- * Fire `handler` once when the component mounts as a result of a
27
- * navigation. Mirror of `useRouteExit` for the entry side.
28
- *
29
- * What this composable covers that an ad-hoc `watch` + `useRoute()`
30
- * doesn't:
31
- *
32
- * - **Skip-initial**: `watch` (with default `immediate: false`) plus the
33
- * explicit `route.transition.from` guard ensures the handler doesn't
34
- * fire on the very first state committed by `router.start()`.
35
- * - **Same-route skip** (default): handler is skipped when
36
- * `route.transition.from === route.name`. Sort/filter/query-only
37
- * navigations re-trigger the watcher, but they are not "entries" in
38
- * the animation / analytics sense. Opt out with
39
- * `skipSameRoute: false`.
40
- * - **Mount-time `route` / `previousRoute` snapshot**: handler receives
41
- * the values that were live at the moment of the new state, not the
42
- * latest ones (which may have moved on if the user navigated again
43
- * before the watcher drained).
44
- *
45
- * **Handler reactivity (Vue):** Vue composables run **once** during
46
- * `setup()`; `handler` is captured in closure at the call site. To vary
47
- * behavior over time, read refs/computeds inside the handler body.
48
- *
49
- * @example Direction-aware entry animation
50
- * ```ts
51
- * useRouteEnter(({ route }) => {
52
- * const direction = route.context.browser?.direction;
53
- * ref.value?.classList.add(
54
- * direction === "back" ? "slide-from-left" : "slide-from-right",
55
- * );
56
- * });
57
- * ```
58
- *
59
- * @example Analytics page-enter event (skip-initial built-in)
60
- * ```ts
61
- * useRouteEnter(({ route, previousRoute }) => {
62
- * analytics.track("page_enter", {
63
- * route: route.name,
64
- * from: previousRoute.name,
65
- * });
66
- * });
67
- * ```
68
- *
69
- * @example Reading rich transition metadata via `route.transition`
70
- * ```ts
71
- * useRouteEnter(({ route }) => {
72
- * if (route.transition.redirected) {
73
- * showToast(`Redirected from ${route.transition.from}`);
74
- * }
75
- * });
76
- * ```
77
- */
78
- export function useRouteEnter(
79
- handler: RouteEnterHandler,
80
- options?: UseRouteEnterOptions,
81
- ): void {
82
- const { route, previousRoute } = useRoute();
83
- const skipSameRoute = options?.skipSameRoute ?? true;
84
- let lastHandledRoute: State | null = null;
85
-
86
- watch(route, (newRoute) => {
87
- const prev = previousRoute.value;
88
-
89
- // Early-exit guards, top-down:
90
- //
91
- // - **Skip-initial**: `!transition.from` catches the first commit
92
- // from `router.start()`. Vue's `watch` (default `immediate: false`)
93
- // does not fire on the initial state — kept for parity with
94
- // React/Preact and v8-ignored.
95
- // - **Skip-same-route**: query-only navigations have
96
- // `transition.from === route.name`. Opt-out via
97
- // `skipSameRoute: false`.
98
- // - **Defensive dedupe + missing `previousRoute`**: same `route`
99
- // ref between watcher activations is unexpected on Vue (driven
100
- // off ref identity); `!prev` is unreachable once
101
- // `transition.from` is set (core populates them together). Both
102
- // kept for parity with React; v8-ignored.
103
- /* v8 ignore start */
104
- if (!newRoute.transition.from) {
105
- return;
106
- }
107
- /* v8 ignore stop */
108
- if (skipSameRoute && newRoute.transition.from === newRoute.name) {
109
- return;
110
- }
111
- /* v8 ignore start */
112
- if (lastHandledRoute === newRoute || !prev) {
113
- return;
114
- }
115
- /* v8 ignore stop */
116
-
117
- lastHandledRoute = newRoute;
118
- handler({ route: newRoute, previousRoute: prev });
119
- });
120
- }
@@ -1,116 +0,0 @@
1
- import { onScopeDispose } from "vue";
2
-
3
- import { useRouter } from "./useRouter";
4
-
5
- import type { State } from "@real-router/core";
6
-
7
- export interface RouteExitContext {
8
- /** The route being left. */
9
- route: State;
10
- /** The route being navigated to. */
11
- nextRoute: State;
12
- /**
13
- * AbortSignal that fires when this navigation is superseded by a later
14
- * one (rapid clicks). Already filtered: when the handler runs,
15
- * `signal.aborted` is guaranteed to be `false`. Use
16
- * `signal.addEventListener("abort", cleanup, { once: true })` for
17
- * cleanup that must run on cancellation.
18
- */
19
- signal: AbortSignal;
20
- }
21
-
22
- export interface UseRouteExitOptions {
23
- /**
24
- * Skip the handler when `route.name === nextRoute.name`
25
- * (sort/filter/query-only navigations on the same route). Default:
26
- * `true`.
27
- */
28
- skipSameRoute?: boolean;
29
- }
30
-
31
- export type RouteExitHandler = (
32
- context: RouteExitContext,
33
- ) => void | Promise<void>;
34
-
35
- /**
36
- * Subscribe to the router's leave-window with the universal guards baked
37
- * in. Wraps `router.subscribeLeave` so consumers don't repeat the same
38
- * boilerplate every time:
39
- *
40
- * - **Reentrant abort pre-check**: if `signal.aborted` is already `true`
41
- * when the handler would run (rapid navigation superseded a slower
42
- * one), the handler is skipped entirely.
43
- * - **Same-route skip**: by default, `route.name === nextRoute.name`
44
- * short-circuits the handler — query-only navigations skip the work.
45
- * Opt out with `skipSameRoute: false`.
46
- *
47
- * Cleanup is bound to the component's effect scope via `onScopeDispose`.
48
- *
49
- * If the handler returns a Promise, the router blocks on it. If the
50
- * Promise resolves, navigation proceeds. If it rejects, the router emits
51
- * `TRANSITION_CANCELLED`.
52
- *
53
- * **Handler reactivity (Vue):** Vue composables run **once** during
54
- * `setup()`; `handler` is captured in closure at the call site. To vary
55
- * behavior over time, read refs/computeds inside the handler body — do
56
- * not rely on swapping the handler reference.
57
- *
58
- * @example Animation
59
- * ```ts
60
- * const ref = useTemplateRef<HTMLDivElement>("box");
61
- *
62
- * useRouteExit(async ({ signal }) => {
63
- * const el = ref.value;
64
- * if (!el) return;
65
- * el.classList.add("fade-out");
66
- * const cleanup = () => el.classList.remove("fade-out");
67
- * signal.addEventListener("abort", cleanup, { once: true });
68
- * try {
69
- * el.getBoundingClientRect();
70
- * await Promise.allSettled(el.getAnimations().map((a) => a.finished));
71
- * } finally {
72
- * cleanup();
73
- * }
74
- * });
75
- * ```
76
- *
77
- * @example Auto-save form draft
78
- * ```ts
79
- * useRouteExit(async ({ signal }) => {
80
- * if (formState.value.dirty) {
81
- * await api.saveDraft(formState.value, { signal });
82
- * }
83
- * });
84
- * ```
85
- *
86
- * @example Reading rich transition metadata via `nextRoute.transition`
87
- * ```ts
88
- * useRouteExit(({ route, nextRoute }) => {
89
- * if (nextRoute.transition.segments.deactivated.includes("products")) {
90
- * productCache.clear();
91
- * }
92
- * if (nextRoute.transition.redirected) return;
93
- * });
94
- * ```
95
- */
96
- export function useRouteExit(
97
- handler: RouteExitHandler,
98
- options?: UseRouteExitOptions,
99
- ): void {
100
- const router = useRouter();
101
- const skipSameRoute = options?.skipSameRoute ?? true;
102
-
103
- const off = router.subscribeLeave(({ route, nextRoute, signal }) => {
104
- if (skipSameRoute && route.name === nextRoute.name) {
105
- return;
106
- }
107
-
108
- if (signal.aborted) {
109
- return;
110
- }
111
-
112
- return handler({ route, nextRoute, signal });
113
- });
114
-
115
- onScopeDispose(off);
116
- }
@@ -1,31 +0,0 @@
1
- import { getNavigator } from "@real-router/core";
2
- import { createRouteNodeSource } from "@real-router/sources";
3
- import { computed } from "vue";
4
-
5
- import { useRefFromSource } from "../useRefFromSource";
6
- import { useRouter } from "./useRouter";
7
-
8
- import type { RouteContext } from "../types";
9
-
10
- export function useRouteNode(nodeName: string): RouteContext {
11
- const router = useRouter();
12
-
13
- const source = createRouteNodeSource(router, nodeName);
14
- const snapshot = useRefFromSource(source);
15
-
16
- // getNavigator is WeakMap-cached in core; no useMemo equivalent needed.
17
- const navigator = getNavigator(router);
18
-
19
- // Derive route/previousRoute via computed instead of mirroring with a sync
20
- // watch into two extra shallowRefs. computed shares snapshot's identity so
21
- // when the underlying source emits the same reference (idempotent or
22
- // out-of-node nav), consumers don't see a new ref.
23
- const route = computed(() => snapshot.value.route);
24
- const previousRoute = computed(() => snapshot.value.previousRoute);
25
-
26
- return {
27
- navigator,
28
- route,
29
- previousRoute,
30
- };
31
- }
@@ -1,12 +0,0 @@
1
- import { getPluginApi } from "@real-router/core/api";
2
- import { getRouteUtils } from "@real-router/route-utils";
3
-
4
- import { useRouter } from "./useRouter";
5
-
6
- import type { RouteUtils } from "@real-router/route-utils";
7
-
8
- export const useRouteUtils = (): RouteUtils => {
9
- const router = useRouter();
10
-
11
- return getRouteUtils(getPluginApi(router).getTree());
12
- };
@@ -1,15 +0,0 @@
1
- import { inject } from "vue";
2
-
3
- import { RouterKey } from "../context";
4
-
5
- import type { Router } from "@real-router/core";
6
-
7
- export const useRouter = (): Router => {
8
- const router = inject(RouterKey);
9
-
10
- if (!router) {
11
- throw new Error("useRouter must be used within a RouterProvider");
12
- }
13
-
14
- return router;
15
- };
@@ -1,14 +0,0 @@
1
- import { getTransitionSource } from "@real-router/sources";
2
-
3
- import { useRefFromSource } from "../useRefFromSource";
4
- import { useRouter } from "./useRouter";
5
-
6
- import type { RouterTransitionSnapshot } from "@real-router/sources";
7
- import type { ShallowRef } from "vue";
8
-
9
- export function useRouterTransition(): ShallowRef<RouterTransitionSnapshot> {
10
- const router = useRouter();
11
- const source = getTransitionSource(router);
12
-
13
- return useRefFromSource(source);
14
- }
package/src/constants.ts DELETED
@@ -1,9 +0,0 @@
1
- /**
2
- * Stable empty object for default params
3
- */
4
- export const EMPTY_PARAMS = Object.freeze({});
5
-
6
- /**
7
- * Stable empty options object
8
- */
9
- export const EMPTY_OPTIONS = Object.freeze({});
package/src/context.ts DELETED
@@ -1,13 +0,0 @@
1
- import type { RouteContext as RouteContextType } from "./types";
2
- import type { HttpStatusSink } from "./utils/createHttpStatusSink";
3
- import type { Router, Navigator } from "@real-router/core";
4
- import type { InjectionKey } from "vue";
5
-
6
- export const RouterKey: InjectionKey<Router> = Symbol("RouterKey");
7
-
8
- export const NavigatorKey: InjectionKey<Navigator> = Symbol("NavigatorKey");
9
-
10
- export const RouteKey: InjectionKey<RouteContextType> = Symbol("RouteKey");
11
-
12
- export const HTTP_STATUS_KEY: InjectionKey<HttpStatusSink> =
13
- Symbol("HttpStatusSink");
@@ -1,31 +0,0 @@
1
- import { NavigatorKey, RouteKey, RouterKey } from "./context";
2
- import { pushDirectiveRouter } from "./directives/vLink";
3
- import { setupRouteProvision } from "./setupRouteProvision";
4
-
5
- import type { Router } from "@real-router/core";
6
- import type { App, Plugin } from "vue";
7
-
8
- export function createRouterPlugin(router: Router): Plugin<[]> {
9
- return {
10
- install(app): void {
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
- }
25
-
26
- app.provide(RouterKey, router);
27
- app.provide(NavigatorKey, navigator);
28
- app.provide(RouteKey, { navigator, route, previousRoute });
29
- },
30
- };
31
- }
@@ -1,208 +0,0 @@
1
- import { shouldNavigate, applyLinkA11y } from "../dom-utils";
2
-
3
- import type { Router, NavigationOptions, Params } from "@real-router/core";
4
- import type { Directive } from "vue";
5
-
6
- export interface LinkDirectiveValue {
7
- name: string;
8
- params?: Params;
9
- options?: NavigationOptions;
10
- }
11
-
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. Not exported from the package entry; retained for
46
- * unit tests and rare standalone-directive setups (where v-link is mounted
47
- * outside any RouterProvider).
48
- *
49
- * @internal
50
- */
51
- export function setDirectiveRouter(router: Router | null): void {
52
- if (router === null) {
53
- routerStack.length = 0;
54
-
55
- return;
56
- }
57
- if (routerStack.length === 0) {
58
- routerStack.push(router);
59
-
60
- return;
61
- }
62
-
63
- routerStack[routerStack.length - 1] = router;
64
- }
65
-
66
- export function getDirectiveRouter(): Router {
67
- const top = routerStack.at(-1);
68
-
69
- if (!top) {
70
- throw new Error(
71
- "v-link directive requires a RouterProvider ancestor. Make sure RouterProvider is mounted.",
72
- );
73
- }
74
-
75
- return top;
76
- }
77
-
78
- interface Handlers {
79
- click: (evt: MouseEvent) => void;
80
- keydown: (evt: KeyboardEvent) => void;
81
- }
82
-
83
- // Single WeakMap halves per-element bookkeeping vs two parallel maps.
84
- const handlers = new WeakMap<HTMLElement, Handlers>();
85
-
86
- /**
87
- * Validates a directive binding value before attaching handlers.
88
- * Returns false (and warns once per call) when the value is missing or
89
- * has no `name` — silently doing nothing is preferable to a runtime crash
90
- * inside a click handler.
91
- */
92
- function isValidBinding(value: unknown): value is LinkDirectiveValue {
93
- if (value === null || value === undefined) {
94
- console.error(
95
- "[real-router] v-link directive received null/undefined value. The element will not be wired for navigation.",
96
- );
97
-
98
- return false;
99
- }
100
- if (typeof (value as { name?: unknown }).name !== "string") {
101
- console.error(
102
- "[real-router] v-link directive value is missing a string `name` field. The element will not be wired for navigation.",
103
- );
104
-
105
- return false;
106
- }
107
-
108
- return true;
109
- }
110
-
111
- function createClickHandler(
112
- router: Router,
113
- value: LinkDirectiveValue,
114
- ): (evt: MouseEvent) => void {
115
- return (evt: MouseEvent) => {
116
- if (!shouldNavigate(evt)) {
117
- return;
118
- }
119
-
120
- evt.preventDefault();
121
- router
122
- .navigate(value.name, value.params ?? {}, value.options ?? {})
123
- .catch(() => {});
124
- };
125
- }
126
-
127
- function createKeydownHandler(
128
- router: Router,
129
- value: LinkDirectiveValue,
130
- element: HTMLElement,
131
- ): (evt: KeyboardEvent) => void {
132
- return (evt: KeyboardEvent) => {
133
- if (evt.key === "Enter" && !(element instanceof HTMLButtonElement)) {
134
- router
135
- .navigate(value.name, value.params ?? {}, value.options ?? {})
136
- .catch(() => {});
137
- }
138
- };
139
- }
140
-
141
- function attachHandlers(
142
- element: HTMLElement,
143
- router: Router,
144
- value: LinkDirectiveValue,
145
- ): void {
146
- const click = createClickHandler(router, value);
147
- const keydown = createKeydownHandler(router, value, element);
148
-
149
- element.addEventListener("click", click);
150
- element.addEventListener("keydown", keydown);
151
-
152
- handlers.set(element, { click, keydown });
153
- }
154
-
155
- function detachHandlers(element: HTMLElement): void {
156
- const entry = handlers.get(element);
157
-
158
- if (entry) {
159
- element.removeEventListener("click", entry.click);
160
- element.removeEventListener("keydown", entry.keydown);
161
- handlers.delete(element);
162
- }
163
- }
164
-
165
- export const vLink: Directive<HTMLElement, LinkDirectiveValue> = {
166
- mounted(element, binding) {
167
- const router = getDirectiveRouter();
168
-
169
- applyLinkA11y(element);
170
-
171
- element.style.cursor = "pointer";
172
-
173
- if (!isValidBinding(binding.value)) {
174
- return;
175
- }
176
-
177
- attachHandlers(element, router, binding.value);
178
- },
179
-
180
- updated(element, binding) {
181
- // Hot-path guard: Vue invokes `updated` on every parent re-render even
182
- // when the directive's binding value reference has not changed. Without
183
- // this short-circuit, every parent rerender (which is the common case on
184
- // Link-heavy pages — any unrelated state change triggers the parent's
185
- // render fn) would detach + reattach the click/keydown listeners.
186
- // Comparing references is enough: when consumers pass a stable
187
- // `LinkDirectiveValue` object (the recommended pattern, since Vue's
188
- // template compiler hoists `v-link="{ name: 'home' }"` to a stable
189
- // literal), this guard collapses the work to zero.
190
- if (binding.value === binding.oldValue) {
191
- return;
192
- }
193
-
194
- const router = getDirectiveRouter();
195
-
196
- detachHandlers(element);
197
-
198
- if (!isValidBinding(binding.value)) {
199
- return;
200
- }
201
-
202
- attachHandlers(element, router, binding.value);
203
- },
204
-
205
- beforeUnmount(element) {
206
- detachHandlers(element);
207
- },
208
- };