@real-router/vue 0.15.1 → 0.15.3
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/dist/cjs/createHttpStatusSink-BENH-5-J.d.ts.map +1 -1
- 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/cjs/ssr.d.ts.map +1 -1
- package/dist/cjs/ssr.js.map +1 -1
- package/dist/cjs/useRoute-Dba_kop6.js.map +1 -1
- package/dist/esm/createHttpStatusSink-BENH-5-J.d.mts.map +1 -1
- 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/dist/esm/ssr.d.mts.map +1 -1
- package/package.json +8 -9
- package/src/RouterProvider.ts +0 -162
- package/src/components/Await.ts +0 -47
- package/src/components/ClientOnly.ts +0 -16
- package/src/components/HttpStatusCode.ts +0 -74
- package/src/components/HttpStatusProvider.ts +0 -22
- package/src/components/Link.ts +0 -226
- package/src/components/RouteView/RouteView.ts +0 -233
- package/src/components/RouteView/components.ts +0 -53
- package/src/components/RouteView/helpers.ts +0 -204
- package/src/components/RouteView/index.ts +0 -8
- package/src/components/RouteView/types.ts +0 -20
- package/src/components/RouterErrorBoundary.ts +0 -61
- package/src/components/ServerOnly.ts +0 -16
- package/src/components/Streamed.ts +0 -31
- package/src/composables/useDeferred.ts +0 -37
- package/src/composables/useIsActiveRoute.ts +0 -61
- package/src/composables/useNavigator.ts +0 -15
- package/src/composables/useRoute.ts +0 -34
- package/src/composables/useRouteEnter.ts +0 -120
- package/src/composables/useRouteExit.ts +0 -116
- package/src/composables/useRouteNode.ts +0 -31
- package/src/composables/useRouteUtils.ts +0 -12
- package/src/composables/useRouter.ts +0 -15
- package/src/composables/useRouterTransition.ts +0 -14
- package/src/constants.ts +0 -9
- package/src/context.ts +0 -13
- package/src/createRouterPlugin.ts +0 -31
- package/src/directives/vLink.ts +0 -208
- package/src/index.ts +0 -64
- package/src/setupRouteProvision.ts +0 -42
- package/src/ssr.ts +0 -39
- package/src/types.ts +0 -40
- package/src/useRefFromSource.ts +0 -16
- 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
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
|
-
}
|
package/src/directives/vLink.ts
DELETED
|
@@ -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
|
-
};
|