@real-router/vue 0.3.3 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/vue",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "type": "commonjs",
5
5
  "description": "Vue 3 integration for Real-Router",
6
6
  "main": "./dist/cjs/index.js",
@@ -8,7 +8,6 @@
8
8
  "types": "./dist/esm/index.d.mts",
9
9
  "exports": {
10
10
  ".": {
11
- "development": "./src/index.ts",
12
11
  "types": {
13
12
  "import": "./dist/esm/index.d.mts",
14
13
  "require": "./dist/cjs/index.d.ts"
@@ -18,7 +17,8 @@
18
17
  }
19
18
  },
20
19
  "files": [
21
- "dist"
20
+ "dist",
21
+ "src"
22
22
  ],
23
23
  "homepage": "https://github.com/greydragon888/real-router",
24
24
  "repository": {
@@ -49,15 +49,15 @@
49
49
  "license": "MIT",
50
50
  "sideEffects": false,
51
51
  "dependencies": {
52
- "@real-router/route-utils": "^0.1.12",
53
- "@real-router/core": "^0.45.1",
54
- "@real-router/sources": "^0.4.2"
52
+ "@real-router/core": "^0.45.2",
53
+ "@real-router/route-utils": "^0.1.13",
54
+ "@real-router/sources": "^0.4.3"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@testing-library/jest-dom": "6.9.1",
58
58
  "@vue/test-utils": "2.4.6",
59
59
  "vue": "3.5.13",
60
- "@real-router/browser-plugin": "^0.11.5",
60
+ "@real-router/browser-plugin": "^0.11.6",
61
61
  "dom-utils": "^0.2.7"
62
62
  },
63
63
  "peerDependencies": {
@@ -69,7 +69,7 @@
69
69
  "test:stress": "vitest run --config vitest.config.stress.mts",
70
70
  "type-check": "tsc --noEmit",
71
71
  "lint": "eslint --cache src/ tests/ --fix --max-warnings 0",
72
- "lint:package": "bash ../../scripts/publint-filter.sh",
72
+ "lint:package": "publint",
73
73
  "lint:types": "attw --pack .",
74
74
  "build:dist-only": "tsdown --config-loader unrun"
75
75
  }
@@ -0,0 +1,69 @@
1
+ import { getNavigator } from "@real-router/core";
2
+ import { createRouteSource } from "@real-router/sources";
3
+ import { createRouteAnnouncer } from "dom-utils";
4
+ import {
5
+ defineComponent,
6
+ onMounted,
7
+ onUnmounted,
8
+ provide,
9
+ shallowRef,
10
+ onScopeDispose,
11
+ } from "vue";
12
+
13
+ import { NavigatorKey, RouteKey, RouterKey } from "./context";
14
+ import { setDirectiveRouter } from "./directives/vLink";
15
+
16
+ import type { Router } from "@real-router/core";
17
+ import type { PropType } from "vue";
18
+
19
+ export const RouterProvider = defineComponent({
20
+ name: "RouterProvider",
21
+ props: {
22
+ router: {
23
+ type: Object as PropType<Router>,
24
+ required: true,
25
+ },
26
+ announceNavigation: {
27
+ type: Boolean,
28
+ default: false,
29
+ },
30
+ },
31
+ setup(props, { slots }) {
32
+ onMounted(() => {
33
+ if (!props.announceNavigation) {
34
+ return;
35
+ }
36
+
37
+ const announcer = createRouteAnnouncer(props.router);
38
+
39
+ onUnmounted(() => {
40
+ announcer.destroy();
41
+ });
42
+ });
43
+
44
+ const navigator = getNavigator(props.router);
45
+
46
+ setDirectiveRouter(props.router);
47
+
48
+ const source = createRouteSource(props.router);
49
+ const initialSnapshot = source.getSnapshot();
50
+
51
+ const route = shallowRef(initialSnapshot.route);
52
+ const previousRoute = shallowRef(initialSnapshot.previousRoute);
53
+
54
+ const unsub = source.subscribe(() => {
55
+ const snapshot = source.getSnapshot();
56
+
57
+ route.value = snapshot.route;
58
+ previousRoute.value = snapshot.previousRoute;
59
+ });
60
+
61
+ onScopeDispose(unsub);
62
+
63
+ provide(RouterKey, props.router);
64
+ provide(NavigatorKey, navigator);
65
+ provide(RouteKey, { navigator, route, previousRoute });
66
+
67
+ return () => slots.default?.();
68
+ },
69
+ });
@@ -0,0 +1,97 @@
1
+ import { shouldNavigate, buildHref, buildActiveClassName } from "dom-utils";
2
+ import { defineComponent, h, computed } from "vue";
3
+
4
+ import { useIsActiveRoute } from "../composables/useIsActiveRoute";
5
+ import { useRouter } from "../composables/useRouter";
6
+ import { EMPTY_PARAMS, EMPTY_OPTIONS } from "../constants";
7
+
8
+ import type { Params, NavigationOptions } from "@real-router/core";
9
+ import type { PropType } from "vue";
10
+
11
+ export const Link = defineComponent({
12
+ name: "Link",
13
+ props: {
14
+ routeName: {
15
+ type: String,
16
+ required: true,
17
+ },
18
+ routeParams: {
19
+ type: Object as PropType<Params>,
20
+ default: () => EMPTY_PARAMS,
21
+ },
22
+ routeOptions: {
23
+ type: Object as PropType<NavigationOptions>,
24
+ default: () => EMPTY_OPTIONS,
25
+ },
26
+ class: {
27
+ type: String,
28
+ default: undefined,
29
+ },
30
+ activeClassName: {
31
+ type: String,
32
+ default: "active",
33
+ },
34
+ activeStrict: {
35
+ type: Boolean,
36
+ default: false,
37
+ },
38
+ ignoreQueryParams: {
39
+ type: Boolean,
40
+ default: true,
41
+ },
42
+ target: {
43
+ type: String,
44
+ default: undefined,
45
+ },
46
+ },
47
+ setup(props, { slots, attrs }) {
48
+ const router = useRouter();
49
+
50
+ const isActive = useIsActiveRoute(
51
+ props.routeName,
52
+ props.routeParams,
53
+ props.activeStrict,
54
+ props.ignoreQueryParams,
55
+ );
56
+
57
+ const href = computed(() =>
58
+ buildHref(router, props.routeName, props.routeParams),
59
+ );
60
+
61
+ const finalClassName = computed(() =>
62
+ buildActiveClassName(isActive.value, props.activeClassName, props.class),
63
+ );
64
+
65
+ const handleClick = (evt: MouseEvent) => {
66
+ if (attrs.onClick && typeof attrs.onClick === "function") {
67
+ (attrs.onClick as (evt: MouseEvent) => void)(evt);
68
+
69
+ if (evt.defaultPrevented) {
70
+ return;
71
+ }
72
+ }
73
+
74
+ if (!shouldNavigate(evt) || props.target === "_blank") {
75
+ return;
76
+ }
77
+
78
+ evt.preventDefault();
79
+ router
80
+ .navigate(props.routeName, props.routeParams, props.routeOptions)
81
+ .catch(() => {});
82
+ };
83
+
84
+ return () =>
85
+ h(
86
+ "a",
87
+ {
88
+ ...attrs,
89
+ href: href.value,
90
+ class: finalClassName.value,
91
+ target: props.target,
92
+ onClick: handleClick,
93
+ },
94
+ slots.default?.(),
95
+ );
96
+ },
97
+ });
@@ -0,0 +1,210 @@
1
+ import {
2
+ Fragment,
3
+ defineComponent,
4
+ h,
5
+ KeepAlive,
6
+ markRaw,
7
+ Suspense,
8
+ } from "vue";
9
+
10
+ import { Match, NotFound } from "./components";
11
+ import { buildRenderList, collectElements } from "./helpers";
12
+ import { useRouteNode } from "../../composables/useRouteNode";
13
+
14
+ import type { Component, VNode } from "vue";
15
+
16
+ type SlotChildren = Record<string, (() => VNode[]) | undefined> | null;
17
+
18
+ function getSlotContent(vnode: VNode): VNode[] | null {
19
+ const slots = vnode.children as SlotChildren;
20
+
21
+ return slots?.default?.() ?? null;
22
+ }
23
+
24
+ function getOrCreateWrapper(
25
+ cache: Map<string, Component>,
26
+ segment: string,
27
+ ): Component {
28
+ const existing = cache.get(segment);
29
+
30
+ if (existing) {
31
+ return existing;
32
+ }
33
+
34
+ const wrapper = markRaw(
35
+ defineComponent({
36
+ name: `KeepAlive-${segment}`,
37
+ setup(_wrapperProps, wrapperCtx) {
38
+ return () => wrapperCtx.slots.default?.();
39
+ },
40
+ }),
41
+ );
42
+
43
+ cache.set(segment, wrapper);
44
+
45
+ return wrapper;
46
+ }
47
+
48
+ function wrapWithSuspense(content: VNode, fallback: unknown): VNode {
49
+ if (fallback === undefined) {
50
+ return content;
51
+ }
52
+
53
+ const fallbackContent =
54
+ typeof fallback === "function" ? (fallback as () => VNode)() : fallback;
55
+
56
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
57
+ const suspenseComponent = Suspense as any;
58
+
59
+ return h(
60
+ suspenseComponent,
61
+ {},
62
+ {
63
+ default: () => content,
64
+ fallback: () => fallbackContent,
65
+ },
66
+ );
67
+ }
68
+
69
+ const emptyKeepAlivePlaceholder = markRaw(
70
+ defineComponent({
71
+ name: "KeepAlive-placeholder",
72
+ render() {
73
+ return null;
74
+ },
75
+ }),
76
+ );
77
+
78
+ function renderWithRootKA(
79
+ activeChild: VNode,
80
+ wrapperCache: Map<string, Component>,
81
+ fallback: unknown,
82
+ ): VNode {
83
+ const activeProps = activeChild.props as { segment?: string } | null;
84
+ const segment = activeProps?.segment ?? "__not-found__";
85
+ const WrapperComponent = getOrCreateWrapper(wrapperCache, segment);
86
+ const slotContent = getSlotContent(activeChild) ?? [];
87
+ const keepAliveContent = h(KeepAlive, null, {
88
+ default: () =>
89
+ h(WrapperComponent, { key: segment }, { default: () => slotContent }),
90
+ });
91
+
92
+ return wrapWithSuspense(keepAliveContent, fallback);
93
+ }
94
+
95
+ function renderWithPerMatchKA(
96
+ activeChild: VNode,
97
+ wrapperCache: Map<string, Component>,
98
+ fallback: unknown,
99
+ ): VNode | null {
100
+ const matchProps = activeChild.props as {
101
+ segment?: string;
102
+ keepAlive?: boolean;
103
+ } | null;
104
+
105
+ if (matchProps?.keepAlive === true && activeChild.type === Match) {
106
+ /* v8 ignore start */
107
+ const segment = matchProps.segment ?? "__not-found__";
108
+ /* v8 ignore stop */
109
+ const WrapperComponent = getOrCreateWrapper(wrapperCache, segment);
110
+ const slotContent = getSlotContent(activeChild) ?? [];
111
+
112
+ return h(Fragment, [
113
+ h(KeepAlive, null, {
114
+ default: () =>
115
+ h(WrapperComponent, { key: segment }, { default: () => slotContent }),
116
+ }),
117
+ ]);
118
+ }
119
+
120
+ const content = getSlotContent(activeChild);
121
+
122
+ /* v8 ignore start */
123
+ if (!content) {
124
+ return null;
125
+ }
126
+ /* v8 ignore stop */
127
+
128
+ return h(Fragment, [
129
+ h(KeepAlive, null, { default: () => h(emptyKeepAlivePlaceholder) }),
130
+ wrapWithSuspense(h(Fragment, content), fallback),
131
+ ]);
132
+ }
133
+
134
+ const RouteViewComponent = defineComponent({
135
+ name: "RouteView",
136
+ props: {
137
+ nodeName: {
138
+ type: String,
139
+ required: true,
140
+ },
141
+ keepAlive: {
142
+ type: Boolean,
143
+ default: false,
144
+ },
145
+ },
146
+ setup(props, { slots }) {
147
+ const routeContext = useRouteNode(props.nodeName);
148
+ const wrapperCache = new Map<string, Component>();
149
+
150
+ return (): VNode | null => {
151
+ const route = routeContext.route.value;
152
+
153
+ if (!route) {
154
+ return null;
155
+ }
156
+
157
+ const elements: VNode[] = [];
158
+
159
+ collectElements(slots.default?.(), elements);
160
+
161
+ const { rendered, fallback } = buildRenderList(
162
+ elements,
163
+ route.name,
164
+ props.nodeName,
165
+ );
166
+
167
+ if (rendered.length === 0) {
168
+ return null;
169
+ }
170
+
171
+ const activeChild = rendered[0];
172
+
173
+ if (props.keepAlive) {
174
+ return renderWithRootKA(activeChild, wrapperCache, fallback);
175
+ }
176
+
177
+ /* v8 ignore start */
178
+ if (activeChild.type !== Match && activeChild.type !== NotFound) {
179
+ return null;
180
+ }
181
+ /* v8 ignore stop */
182
+
183
+ const hasPerMatchKA = elements.some(
184
+ (element) =>
185
+ element.type === Match &&
186
+ (element.props as { keepAlive?: boolean } | null)?.keepAlive === true,
187
+ );
188
+
189
+ if (hasPerMatchKA) {
190
+ return renderWithPerMatchKA(activeChild, wrapperCache, fallback);
191
+ }
192
+
193
+ const content = getSlotContent(activeChild);
194
+
195
+ if (!content) {
196
+ return null;
197
+ }
198
+
199
+ return wrapWithSuspense(h(Fragment, content), fallback);
200
+ };
201
+ },
202
+ });
203
+
204
+ export const RouteView = Object.assign(RouteViewComponent, { Match, NotFound });
205
+
206
+ export type {
207
+ RouteViewProps,
208
+ MatchProps as RouteViewMatchProps,
209
+ NotFoundProps as RouteViewNotFoundProps,
210
+ } from "./types";
@@ -0,0 +1,35 @@
1
+ import { defineComponent } from "vue";
2
+
3
+ import type { PropType, VNode } from "vue";
4
+
5
+ function renderNull() {
6
+ return null;
7
+ }
8
+
9
+ export const Match = defineComponent({
10
+ name: "RouteView.Match",
11
+ props: {
12
+ segment: {
13
+ type: String as PropType<string>,
14
+ required: true,
15
+ },
16
+ exact: {
17
+ type: Boolean,
18
+ default: false,
19
+ },
20
+ fallback: {
21
+ type: [Object, Function] as PropType<VNode | (() => VNode)>,
22
+ default: undefined,
23
+ },
24
+ keepAlive: {
25
+ type: Boolean,
26
+ default: false,
27
+ },
28
+ },
29
+ render: renderNull,
30
+ });
31
+
32
+ export const NotFound = defineComponent({
33
+ name: "RouteView.NotFound",
34
+ render: renderNull,
35
+ });
@@ -0,0 +1,110 @@
1
+ import { UNKNOWN_ROUTE } from "@real-router/core";
2
+ import { startsWithSegment } from "@real-router/route-utils";
3
+ import { Fragment, isVNode } from "vue";
4
+
5
+ import { Match, NotFound } from "./components";
6
+
7
+ import type { VNode } from "vue";
8
+
9
+ type FallbackType = VNode | (() => VNode) | undefined;
10
+
11
+ function isSegmentMatch(
12
+ routeName: string,
13
+ fullSegmentName: string,
14
+ exact: boolean,
15
+ ): boolean {
16
+ if (exact) {
17
+ return routeName === fullSegmentName;
18
+ }
19
+
20
+ return startsWithSegment(routeName, fullSegmentName);
21
+ }
22
+
23
+ function normalizeChildren(children: unknown): VNode[] {
24
+ if (Array.isArray(children)) {
25
+ const result: VNode[] = [];
26
+
27
+ for (const child of children) {
28
+ if (Array.isArray(child)) {
29
+ result.push(...normalizeChildren(child));
30
+ } else if (isVNode(child)) {
31
+ result.push(child);
32
+ }
33
+ }
34
+
35
+ return result;
36
+ }
37
+
38
+ if (isVNode(children)) {
39
+ return [children];
40
+ }
41
+
42
+ return [];
43
+ }
44
+
45
+ export function collectElements(children: unknown, result: VNode[]): void {
46
+ const vnodes = normalizeChildren(children);
47
+
48
+ for (const child of vnodes) {
49
+ if (child.type === Match || child.type === NotFound) {
50
+ result.push(child);
51
+ } else if (child.type === Fragment) {
52
+ collectElements(child.children, result);
53
+ }
54
+ }
55
+ }
56
+
57
+ export function buildRenderList(
58
+ elements: VNode[],
59
+ routeName: string,
60
+ nodeName: string,
61
+ ): {
62
+ rendered: VNode[];
63
+ activeMatchFound: boolean;
64
+ fallback?: FallbackType;
65
+ } {
66
+ let notFoundChildren: unknown = null;
67
+ let activeMatchFound = false;
68
+ let fallback: FallbackType = undefined;
69
+ const rendered: VNode[] = [];
70
+
71
+ for (const child of elements) {
72
+ if (child.type === NotFound) {
73
+ notFoundChildren = child.children;
74
+ continue;
75
+ }
76
+
77
+ const props = child.props as {
78
+ segment: string;
79
+ exact?: boolean;
80
+ fallback?: FallbackType;
81
+ } | null;
82
+ const segment = props?.segment ?? "";
83
+ const exact = props?.exact ?? false;
84
+ const fullSegmentName = nodeName ? `${nodeName}.${segment}` : segment;
85
+ const isActive =
86
+ !activeMatchFound && isSegmentMatch(routeName, fullSegmentName, exact);
87
+
88
+ if (isActive) {
89
+ activeMatchFound = true;
90
+ fallback = props?.fallback;
91
+ rendered.push(child);
92
+ }
93
+ }
94
+
95
+ if (
96
+ !activeMatchFound &&
97
+ routeName === UNKNOWN_ROUTE &&
98
+ notFoundChildren !== null
99
+ ) {
100
+ const nfElements = elements.filter((element) => element.type === NotFound);
101
+ /* v8 ignore next 3 */
102
+ const lastNf = nfElements.at(-1);
103
+
104
+ if (lastNf) {
105
+ rendered.push(lastNf);
106
+ }
107
+ }
108
+
109
+ return { rendered, activeMatchFound, fallback };
110
+ }
@@ -0,0 +1,7 @@
1
+ export { RouteView } from "./RouteView";
2
+
3
+ export type {
4
+ RouteViewProps,
5
+ RouteViewMatchProps,
6
+ RouteViewNotFoundProps,
7
+ } from "./RouteView";
@@ -0,0 +1,15 @@
1
+ import type { VNode } from "vue";
2
+
3
+ export interface RouteViewProps {
4
+ readonly nodeName: string;
5
+ readonly keepAlive?: boolean;
6
+ }
7
+
8
+ export interface MatchProps {
9
+ readonly segment: string;
10
+ readonly exact?: boolean;
11
+ readonly fallback?: VNode | (() => VNode);
12
+ readonly keepAlive?: boolean;
13
+ }
14
+
15
+ export type NotFoundProps = Record<string, never>;
@@ -0,0 +1,69 @@
1
+ import { defineComponent, h, ref, watch, computed, Fragment } from "vue";
2
+
3
+ import { useRouterError } from "../composables/useRouterError";
4
+
5
+ import type { RouterError, State } from "@real-router/core";
6
+ import type { VNode, PropType } from "vue";
7
+
8
+ export const RouterErrorBoundary = defineComponent({
9
+ name: "RouterErrorBoundary",
10
+ props: {
11
+ fallback: {
12
+ type: Function as PropType<
13
+ (error: RouterError, resetError: () => void) => VNode
14
+ >,
15
+ required: true,
16
+ },
17
+ onError: {
18
+ type: Function as PropType<
19
+ (
20
+ error: RouterError,
21
+ toRoute: State | null,
22
+ fromRoute: State | null,
23
+ ) => void
24
+ >,
25
+ default: undefined,
26
+ },
27
+ },
28
+ setup(props, { slots }) {
29
+ const snapshot = useRouterError();
30
+ const dismissedVersion = ref(-1);
31
+
32
+ watch(
33
+ () => snapshot.value.version,
34
+ () => {
35
+ if (snapshot.value.error) {
36
+ props.onError?.(
37
+ snapshot.value.error,
38
+ snapshot.value.toRoute,
39
+ snapshot.value.fromRoute,
40
+ );
41
+ }
42
+ },
43
+ { immediate: true },
44
+ );
45
+
46
+ const visibleError = computed(() =>
47
+ snapshot.value.version > dismissedVersion.value
48
+ ? snapshot.value.error
49
+ : null,
50
+ );
51
+
52
+ const resetError = () => {
53
+ dismissedVersion.value = snapshot.value.version;
54
+ };
55
+
56
+ return () => {
57
+ const children = slots.default?.() ?? [];
58
+ const errorVNode = visibleError.value
59
+ ? props.fallback(visibleError.value, resetError)
60
+ : null;
61
+
62
+ return h(Fragment, null, [...children, errorVNode]);
63
+ };
64
+ },
65
+ });
66
+
67
+ export type RouterErrorBoundaryProps = InstanceType<
68
+ typeof RouterErrorBoundary
69
+ >["$props"];
@@ -0,0 +1,23 @@
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
+ export function useIsActiveRoute(
10
+ routeName: string,
11
+ params?: Params,
12
+ strict = false,
13
+ ignoreQueryParams = true,
14
+ ): ShallowRef<boolean> {
15
+ const router = useRouter();
16
+
17
+ const source = createActiveRouteSource(router, routeName, params, {
18
+ strict,
19
+ ignoreQueryParams,
20
+ });
21
+
22
+ return useRefFromSource(source);
23
+ }
@@ -0,0 +1,15 @@
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
+ };
@@ -0,0 +1,15 @@
1
+ import { inject } from "vue";
2
+
3
+ import { RouteKey } from "../context";
4
+
5
+ import type { RouteContext } from "../types";
6
+
7
+ export const useRoute = (): RouteContext => {
8
+ const routeContext = inject(RouteKey);
9
+
10
+ if (!routeContext) {
11
+ throw new Error("useRoute must be used within a RouterProvider");
12
+ }
13
+
14
+ return routeContext;
15
+ };
@@ -0,0 +1,38 @@
1
+ import { getNavigator } from "@real-router/core";
2
+ import { createRouteNodeSource } from "@real-router/sources";
3
+ import { shallowRef, watch } from "vue";
4
+
5
+ import { useRefFromSource } from "../useRefFromSource";
6
+ import { useRouter } from "./useRouter";
7
+
8
+ import type { RouteContext } from "../types";
9
+ import type { State } from "@real-router/core";
10
+
11
+ export function useRouteNode(nodeName: string): RouteContext {
12
+ const router = useRouter();
13
+
14
+ const source = createRouteNodeSource(router, nodeName);
15
+ const snapshot = useRefFromSource(source);
16
+
17
+ const navigator = getNavigator(router);
18
+
19
+ const route = shallowRef<State | undefined>(snapshot.value.route);
20
+ const previousRoute = shallowRef<State | undefined>(
21
+ snapshot.value.previousRoute,
22
+ );
23
+
24
+ watch(
25
+ snapshot,
26
+ (newSnapshot) => {
27
+ route.value = newSnapshot.route;
28
+ previousRoute.value = newSnapshot.previousRoute;
29
+ },
30
+ { flush: "sync" },
31
+ );
32
+
33
+ return {
34
+ navigator,
35
+ route,
36
+ previousRoute,
37
+ };
38
+ }
@@ -0,0 +1,12 @@
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
+ };
@@ -0,0 +1,15 @@
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
+ };
@@ -0,0 +1,23 @@
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
+ }
@@ -0,0 +1,15 @@
1
+ import { createTransitionSource } 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
+
12
+ const source = createTransitionSource(router);
13
+
14
+ return useRefFromSource(source);
15
+ }
@@ -0,0 +1,9 @@
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 ADDED
@@ -0,0 +1,9 @@
1
+ import type { RouteContext as RouteContextType } from "./types";
2
+ import type { Router, Navigator } from "@real-router/core";
3
+ import type { InjectionKey } from "vue";
4
+
5
+ export const RouterKey: InjectionKey<Router> = Symbol("RouterKey");
6
+
7
+ export const NavigatorKey: InjectionKey<Navigator> = Symbol("NavigatorKey");
8
+
9
+ export const RouteKey: InjectionKey<RouteContextType> = Symbol("RouteKey");
@@ -0,0 +1,36 @@
1
+ import { getNavigator } from "@real-router/core";
2
+ import { createRouteSource } from "@real-router/sources";
3
+ import { shallowRef } from "vue";
4
+
5
+ import { NavigatorKey, RouteKey, RouterKey } from "./context";
6
+ import { setDirectiveRouter } from "./directives/vLink";
7
+
8
+ import type { Router } from "@real-router/core";
9
+ import type { Plugin } from "vue";
10
+
11
+ export function createRouterPlugin(router: Router): Plugin<[]> {
12
+ return {
13
+ install(app): void {
14
+ const navigator = getNavigator(router);
15
+
16
+ setDirectiveRouter(router);
17
+
18
+ const source = createRouteSource(router);
19
+ const initialSnapshot = source.getSnapshot();
20
+
21
+ const route = shallowRef(initialSnapshot.route);
22
+ const previousRoute = shallowRef(initialSnapshot.previousRoute);
23
+
24
+ source.subscribe(() => {
25
+ const snapshot = source.getSnapshot();
26
+
27
+ route.value = snapshot.route;
28
+ previousRoute.value = snapshot.previousRoute;
29
+ });
30
+
31
+ app.provide(RouterKey, router);
32
+ app.provide(NavigatorKey, navigator);
33
+ app.provide(RouteKey, { navigator, route, previousRoute });
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,116 @@
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
+ let _router: Router | null = null;
13
+
14
+ export function setDirectiveRouter(router: Router): void {
15
+ _router = router;
16
+ }
17
+
18
+ export function getDirectiveRouter(): Router {
19
+ if (!_router) {
20
+ /* v8 ignore next 3 -- @preserve Defensive: router always initialized by RouterProvider */
21
+ throw new Error(
22
+ "v-link directive requires a RouterProvider ancestor. Make sure RouterProvider is mounted.",
23
+ );
24
+ }
25
+
26
+ return _router;
27
+ }
28
+
29
+ const clickHandlers = new WeakMap<HTMLElement, (evt: MouseEvent) => void>();
30
+ const keydownHandlers = new WeakMap<
31
+ HTMLElement,
32
+ (evt: KeyboardEvent) => void
33
+ >();
34
+
35
+ function createClickHandler(
36
+ router: Router,
37
+ value: LinkDirectiveValue,
38
+ ): (evt: MouseEvent) => void {
39
+ return (evt: MouseEvent) => {
40
+ if (!shouldNavigate(evt)) {
41
+ return;
42
+ }
43
+
44
+ evt.preventDefault();
45
+ router
46
+ .navigate(value.name, value.params ?? {}, value.options ?? {})
47
+ .catch(() => {});
48
+ };
49
+ }
50
+
51
+ function createKeydownHandler(
52
+ router: Router,
53
+ value: LinkDirectiveValue,
54
+ element: HTMLElement,
55
+ ): (evt: KeyboardEvent) => void {
56
+ return (evt: KeyboardEvent) => {
57
+ if (evt.key === "Enter" && !(element instanceof HTMLButtonElement)) {
58
+ router
59
+ .navigate(value.name, value.params ?? {}, value.options ?? {})
60
+ .catch(() => {});
61
+ }
62
+ };
63
+ }
64
+
65
+ function attachHandlers(
66
+ element: HTMLElement,
67
+ router: Router,
68
+ value: LinkDirectiveValue,
69
+ ): void {
70
+ const handleClick = createClickHandler(router, value);
71
+ const handleKeyDown = createKeydownHandler(router, value, element);
72
+
73
+ element.addEventListener("click", handleClick);
74
+ element.addEventListener("keydown", handleKeyDown);
75
+
76
+ clickHandlers.set(element, handleClick);
77
+ keydownHandlers.set(element, handleKeyDown);
78
+ }
79
+
80
+ function detachHandlers(element: HTMLElement): void {
81
+ const clickHandler = clickHandlers.get(element);
82
+ const keydownHandler = keydownHandlers.get(element);
83
+
84
+ if (clickHandler) {
85
+ element.removeEventListener("click", clickHandler);
86
+ }
87
+ if (keydownHandler) {
88
+ element.removeEventListener("keydown", keydownHandler);
89
+ }
90
+
91
+ clickHandlers.delete(element);
92
+ keydownHandlers.delete(element);
93
+ }
94
+
95
+ export const vLink: Directive<HTMLElement, LinkDirectiveValue> = {
96
+ mounted(element, binding) {
97
+ const router = getDirectiveRouter();
98
+
99
+ applyLinkA11y(element);
100
+
101
+ element.style.cursor = "pointer";
102
+
103
+ attachHandlers(element, router, binding.value);
104
+ },
105
+
106
+ updated(element, binding) {
107
+ const router = getDirectiveRouter();
108
+
109
+ detachHandlers(element);
110
+ attachHandlers(element, router, binding.value);
111
+ },
112
+
113
+ beforeUnmount(element) {
114
+ detachHandlers(element);
115
+ },
116
+ };
package/src/index.ts ADDED
@@ -0,0 +1,47 @@
1
+ // Components
2
+ export { RouteView } from "./components/RouteView";
3
+
4
+ export { Link } from "./components/Link";
5
+
6
+ export { RouterErrorBoundary } from "./components/RouterErrorBoundary";
7
+
8
+ // Directives
9
+ export { vLink } from "./directives/vLink";
10
+
11
+ // Composables
12
+ export { useRouter } from "./composables/useRouter";
13
+
14
+ export { useNavigator } from "./composables/useNavigator";
15
+
16
+ export { useRouteUtils } from "./composables/useRouteUtils";
17
+
18
+ export { useRoute } from "./composables/useRoute";
19
+
20
+ export { useRouteNode } from "./composables/useRouteNode";
21
+
22
+ export { useRouterTransition } from "./composables/useRouterTransition";
23
+
24
+ // Plugin
25
+ export { createRouterPlugin } from "./createRouterPlugin";
26
+
27
+ // Context
28
+ export { RouterProvider } from "./RouterProvider";
29
+
30
+ export { RouterKey, NavigatorKey, RouteKey } from "./context";
31
+
32
+ // Types
33
+ export type { LinkProps } from "./types";
34
+
35
+ export type { RouterErrorBoundaryProps } from "./components/RouterErrorBoundary";
36
+
37
+ export type { LinkDirectiveValue } from "./directives/vLink";
38
+
39
+ export type {
40
+ RouteViewProps,
41
+ RouteViewMatchProps,
42
+ RouteViewNotFoundProps,
43
+ } from "./components/RouteView";
44
+
45
+ export type { Navigator } from "@real-router/core";
46
+
47
+ export type { RouterTransitionSnapshot } from "@real-router/sources";
package/src/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type {
2
+ NavigationOptions,
3
+ Params,
4
+ Navigator,
5
+ State,
6
+ } from "@real-router/core";
7
+ import type { ShallowRef } from "vue";
8
+
9
+ export interface RouteContext {
10
+ navigator: Navigator;
11
+ route: ShallowRef<State | undefined>;
12
+ previousRoute: ShallowRef<State | undefined>;
13
+ }
14
+
15
+ export interface LinkProps<P extends Params = Params> {
16
+ routeName: string;
17
+ routeParams?: P;
18
+ routeOptions?: NavigationOptions;
19
+ class?: string;
20
+ activeClassName?: string;
21
+ activeStrict?: boolean;
22
+ ignoreQueryParams?: boolean;
23
+ target?: string;
24
+ }
@@ -0,0 +1,16 @@
1
+ import { shallowRef, onScopeDispose } from "vue";
2
+
3
+ import type { RouterSource } from "@real-router/sources";
4
+ import type { ShallowRef } from "vue";
5
+
6
+ export function useRefFromSource<T>(source: RouterSource<T>): ShallowRef<T> {
7
+ const ref = shallowRef(source.getSnapshot());
8
+
9
+ const unsub = source.subscribe(() => {
10
+ ref.value = source.getSnapshot();
11
+ });
12
+
13
+ onScopeDispose(unsub);
14
+
15
+ return ref;
16
+ }