@real-router/vue 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,152 @@
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 RouteViewComponent = defineComponent({
70
+ name: "RouteView",
71
+ props: {
72
+ nodeName: {
73
+ type: String,
74
+ required: true,
75
+ },
76
+ keepAlive: {
77
+ type: Boolean,
78
+ default: false,
79
+ },
80
+ },
81
+ setup(props, { slots }) {
82
+ const routeContext = useRouteNode(props.nodeName);
83
+
84
+ const wrapperCache = new Map<string, Component>();
85
+
86
+ return (): VNode | null => {
87
+ const route = routeContext.route.value;
88
+
89
+ if (!route) {
90
+ return null;
91
+ }
92
+
93
+ const elements: VNode[] = [];
94
+
95
+ collectElements(slots.default?.(), elements);
96
+
97
+ const { rendered, fallback } = buildRenderList(
98
+ elements,
99
+ route.name,
100
+ props.nodeName,
101
+ );
102
+
103
+ if (rendered.length === 0) {
104
+ return null;
105
+ }
106
+
107
+ const activeChild = rendered[0];
108
+
109
+ if (!props.keepAlive) {
110
+ if (activeChild.type === Match || activeChild.type === NotFound) {
111
+ const content = getSlotContent(activeChild);
112
+
113
+ if (!content) {
114
+ return null;
115
+ }
116
+
117
+ const fragment = h(Fragment, content);
118
+
119
+ return wrapWithSuspense(fragment, fallback);
120
+ }
121
+
122
+ /* v8 ignore start */
123
+ return null;
124
+ /* v8 ignore stop */
125
+ }
126
+
127
+ const activeProps = activeChild.props as {
128
+ segment?: string;
129
+ } | null;
130
+ const segment = activeProps?.segment ?? "__not-found__";
131
+
132
+ const WrapperComponent = getOrCreateWrapper(wrapperCache, segment);
133
+ /* v8 ignore next */
134
+ const slotContent = getSlotContent(activeChild) ?? [];
135
+
136
+ const keepAliveContent = h(KeepAlive, null, {
137
+ default: () =>
138
+ h(WrapperComponent, { key: segment }, { default: () => slotContent }),
139
+ });
140
+
141
+ return wrapWithSuspense(keepAliveContent, fallback);
142
+ };
143
+ },
144
+ });
145
+
146
+ export const RouteView = Object.assign(RouteViewComponent, { Match, NotFound });
147
+
148
+ export type {
149
+ RouteViewProps,
150
+ MatchProps as RouteViewMatchProps,
151
+ NotFoundProps as RouteViewNotFoundProps,
152
+ } from "./types";
@@ -0,0 +1,31 @@
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
+ },
25
+ render: renderNull,
26
+ });
27
+
28
+ export const NotFound = defineComponent({
29
+ name: "RouteView.NotFound",
30
+ render: renderNull,
31
+ });
@@ -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((e) => e.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,14 @@
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
+ }
13
+
14
+ export type NotFoundProps = Record<string, never>;
@@ -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,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,37 @@
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
+ // eslint-disable-next-line sonarjs/function-return-type -- Plugin<[]> return type is consistent
12
+ export function createRouterPlugin(router: Router): Plugin<[]> {
13
+ return {
14
+ install(app): void {
15
+ const navigator = getNavigator(router);
16
+
17
+ setDirectiveRouter(router);
18
+
19
+ const source = createRouteSource(router);
20
+ const initialSnapshot = source.getSnapshot();
21
+
22
+ const route = shallowRef(initialSnapshot.route);
23
+ const previousRoute = shallowRef(initialSnapshot.previousRoute);
24
+
25
+ source.subscribe(() => {
26
+ const snapshot = source.getSnapshot();
27
+
28
+ route.value = snapshot.route;
29
+ previousRoute.value = snapshot.previousRoute;
30
+ });
31
+
32
+ app.provide(RouterKey, router);
33
+ app.provide(NavigatorKey, navigator);
34
+ app.provide(RouteKey, { navigator, route, previousRoute });
35
+ },
36
+ };
37
+ }