@esmx/router-react 3.0.0-rc.105
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/README.md +515 -0
- package/dist/context.d.ts +85 -0
- package/dist/context.mjs +23 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +11 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.mjs +169 -0
- package/dist/router-link.d.ts +35 -0
- package/dist/router-link.mjs +85 -0
- package/dist/router-provider.d.ts +61 -0
- package/dist/router-provider.mjs +34 -0
- package/dist/router-view.d.ts +63 -0
- package/dist/router-view.mjs +40 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.mjs +0 -0
- package/dist/use-link.d.ts +97 -0
- package/dist/use-link.mjs +40 -0
- package/dist/util.d.ts +13 -0
- package/dist/util.mjs +15 -0
- package/package.json +85 -0
- package/src/context.ts +109 -0
- package/src/index.test.ts +215 -0
- package/src/index.ts +21 -0
- package/src/router-link.ts +222 -0
- package/src/router-provider.ts +104 -0
- package/src/router-view.ts +113 -0
- package/src/types.ts +30 -0
- package/src/use-link.ts +138 -0
- package/src/util.ts +38 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import * as RouterReactModule from "./index.mjs";
|
|
3
|
+
describe("index.ts - Package Entry Point", () => {
|
|
4
|
+
describe("Hook Exports", () => {
|
|
5
|
+
it("should export useRouter hook", () => {
|
|
6
|
+
expect(RouterReactModule.useRouter).toBeDefined();
|
|
7
|
+
expect(typeof RouterReactModule.useRouter).toBe("function");
|
|
8
|
+
});
|
|
9
|
+
it("should export useRoute hook", () => {
|
|
10
|
+
expect(RouterReactModule.useRoute).toBeDefined();
|
|
11
|
+
expect(typeof RouterReactModule.useRoute).toBe("function");
|
|
12
|
+
});
|
|
13
|
+
it("should export useLink hook", () => {
|
|
14
|
+
expect(RouterReactModule.useLink).toBeDefined();
|
|
15
|
+
expect(typeof RouterReactModule.useLink).toBe("function");
|
|
16
|
+
});
|
|
17
|
+
it("should export useRouterViewDepth hook", () => {
|
|
18
|
+
expect(RouterReactModule.useRouterViewDepth).toBeDefined();
|
|
19
|
+
expect(typeof RouterReactModule.useRouterViewDepth).toBe(
|
|
20
|
+
"function"
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe("Component Exports", () => {
|
|
25
|
+
it("should export RouterProvider component", () => {
|
|
26
|
+
expect(RouterReactModule.RouterProvider).toBeDefined();
|
|
27
|
+
expect(typeof RouterReactModule.RouterProvider).toBe("function");
|
|
28
|
+
});
|
|
29
|
+
it("should export RouterLink component", () => {
|
|
30
|
+
expect(RouterReactModule.RouterLink).toBeDefined();
|
|
31
|
+
expect(typeof RouterReactModule.RouterLink).toBe("object");
|
|
32
|
+
});
|
|
33
|
+
it("should export RouterView component", () => {
|
|
34
|
+
expect(RouterReactModule.RouterView).toBeDefined();
|
|
35
|
+
expect(typeof RouterReactModule.RouterView).toBe("function");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
describe("Context Exports", () => {
|
|
39
|
+
it("should export RouterContext", () => {
|
|
40
|
+
expect(RouterReactModule.RouterContext).toBeDefined();
|
|
41
|
+
expect(RouterReactModule.RouterContext.displayName).toBe(
|
|
42
|
+
"RouterContext"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
it("should export RouterViewDepthContext", () => {
|
|
46
|
+
expect(RouterReactModule.RouterViewDepthContext).toBeDefined();
|
|
47
|
+
expect(RouterReactModule.RouterViewDepthContext.displayName).toBe(
|
|
48
|
+
"RouterViewDepthContext"
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe("Export Completeness", () => {
|
|
53
|
+
it("should export all expected functions and components", () => {
|
|
54
|
+
const expectedExports = [
|
|
55
|
+
// Hooks
|
|
56
|
+
"useRouter",
|
|
57
|
+
"useRoute",
|
|
58
|
+
"useLink",
|
|
59
|
+
"useRouterViewDepth",
|
|
60
|
+
// Components
|
|
61
|
+
"RouterProvider",
|
|
62
|
+
"RouterLink",
|
|
63
|
+
"RouterView",
|
|
64
|
+
// Context
|
|
65
|
+
"RouterContext",
|
|
66
|
+
"RouterViewDepthContext"
|
|
67
|
+
];
|
|
68
|
+
expectedExports.forEach((exportName) => {
|
|
69
|
+
expect(RouterReactModule).toHaveProperty(exportName);
|
|
70
|
+
expect(Object.hasOwn(RouterReactModule, exportName)).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
it("should not export unexpected items", () => {
|
|
74
|
+
const actualExports = Object.keys(RouterReactModule);
|
|
75
|
+
const expectedExports = [
|
|
76
|
+
"useRouter",
|
|
77
|
+
"useRoute",
|
|
78
|
+
"useLink",
|
|
79
|
+
"useRouterViewDepth",
|
|
80
|
+
"RouterProvider",
|
|
81
|
+
"RouterLink",
|
|
82
|
+
"RouterView",
|
|
83
|
+
"RouterContext",
|
|
84
|
+
"RouterViewDepthContext"
|
|
85
|
+
];
|
|
86
|
+
const unexpectedExports = actualExports.filter(
|
|
87
|
+
(exportName) => !expectedExports.includes(exportName)
|
|
88
|
+
);
|
|
89
|
+
expect(unexpectedExports).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("Hook Error Handling", () => {
|
|
93
|
+
it("hooks should throw when used incorrectly", () => {
|
|
94
|
+
expect(() => {
|
|
95
|
+
RouterReactModule.useRouter();
|
|
96
|
+
}).toThrow();
|
|
97
|
+
expect(() => {
|
|
98
|
+
RouterReactModule.useRoute();
|
|
99
|
+
}).toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("Component Properties", () => {
|
|
103
|
+
it("should have RouterProvider with displayName", () => {
|
|
104
|
+
expect(RouterReactModule.RouterProvider.displayName).toBe(
|
|
105
|
+
"RouterProvider"
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
it("should have RouterView with displayName", () => {
|
|
109
|
+
expect(RouterReactModule.RouterView.displayName).toBe("RouterView");
|
|
110
|
+
});
|
|
111
|
+
it("should have RouterLink with displayName", () => {
|
|
112
|
+
expect(RouterReactModule.RouterLink.displayName).toBe("RouterLink");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe("Module Structure", () => {
|
|
116
|
+
it("should be a proper ES module", () => {
|
|
117
|
+
expect(typeof RouterReactModule).toBe("object");
|
|
118
|
+
expect(RouterReactModule).not.toBeNull();
|
|
119
|
+
expect("default" in RouterReactModule).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
it("should have consistent export naming", () => {
|
|
122
|
+
const hookExports = [
|
|
123
|
+
"useRouter",
|
|
124
|
+
"useRoute",
|
|
125
|
+
"useLink",
|
|
126
|
+
"useRouterViewDepth"
|
|
127
|
+
];
|
|
128
|
+
hookExports.forEach((exportName) => {
|
|
129
|
+
expect(exportName).toMatch(/^use[A-Z][a-zA-Z]*$/);
|
|
130
|
+
});
|
|
131
|
+
const componentExports = [
|
|
132
|
+
"RouterProvider",
|
|
133
|
+
"RouterLink",
|
|
134
|
+
"RouterView",
|
|
135
|
+
"RouterContext",
|
|
136
|
+
"RouterViewDepthContext"
|
|
137
|
+
];
|
|
138
|
+
componentExports.forEach((exportName) => {
|
|
139
|
+
expect(exportName).toMatch(/^[A-Z][a-zA-Z]*$/);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("TypeScript Types", () => {
|
|
144
|
+
it("should export RouterContextValue type (via typeof)", () => {
|
|
145
|
+
const context = RouterReactModule.RouterContext;
|
|
146
|
+
expect(context).toBeDefined();
|
|
147
|
+
expect(context.Provider).toBeDefined();
|
|
148
|
+
expect(context.Consumer).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
it("should have RouterViewDepthContext with correct default value", () => {
|
|
151
|
+
const context = RouterReactModule.RouterViewDepthContext;
|
|
152
|
+
expect(context).toBeDefined();
|
|
153
|
+
expect(context.Provider).toBeDefined();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe("React Best Practices", () => {
|
|
157
|
+
it("RouterLink should be a forwardRef component", () => {
|
|
158
|
+
const link = RouterReactModule.RouterLink;
|
|
159
|
+
expect(link.$$typeof).toBeDefined();
|
|
160
|
+
expect(link.render || link.type).toBeDefined();
|
|
161
|
+
});
|
|
162
|
+
it("RouterProvider should accept router and children props", () => {
|
|
163
|
+
expect(RouterReactModule.RouterProvider).toBeDefined();
|
|
164
|
+
});
|
|
165
|
+
it("RouterView should accept fallback prop", () => {
|
|
166
|
+
expect(RouterReactModule.RouterView).toBeDefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RouteLayerOptions, RouteLocationInput, RouteMatchType, RouterLinkType } from '@esmx/router';
|
|
2
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* Props for RouterLink component.
|
|
5
|
+
* Similar to RouterLinkProps from @esmx/router with React-specific additions.
|
|
6
|
+
*/
|
|
7
|
+
export interface RouterLinkComponentProps {
|
|
8
|
+
/** Target route location (required) */
|
|
9
|
+
to: RouteLocationInput;
|
|
10
|
+
/** Navigation type */
|
|
11
|
+
type?: RouterLinkType;
|
|
12
|
+
/** @deprecated Use type='replace' instead */
|
|
13
|
+
replace?: boolean;
|
|
14
|
+
/** Active matching mode */
|
|
15
|
+
exact?: RouteMatchType;
|
|
16
|
+
/** CSS class for active state */
|
|
17
|
+
activeClass?: string;
|
|
18
|
+
/** Event(s) that trigger navigation */
|
|
19
|
+
event?: string | string[];
|
|
20
|
+
/** HTML tag to render */
|
|
21
|
+
tag?: string;
|
|
22
|
+
/** Layer navigation options */
|
|
23
|
+
layerOptions?: RouteLayerOptions;
|
|
24
|
+
/** Hook function called before navigation */
|
|
25
|
+
beforeNavigate?: (event: Event, eventName: string) => void;
|
|
26
|
+
/** Link content */
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
/** Additional CSS class name */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Inline styles */
|
|
31
|
+
style?: CSSProperties;
|
|
32
|
+
}
|
|
33
|
+
export declare const RouterLink: React.ForwardRefExoticComponent<RouterLinkComponentProps & {
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
} & React.RefAttributes<HTMLAnchorElement>>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createElement,
|
|
3
|
+
forwardRef,
|
|
4
|
+
useCallback,
|
|
5
|
+
useMemo
|
|
6
|
+
} from "react";
|
|
7
|
+
import { useRouter } from "./context.mjs";
|
|
8
|
+
function RouterLinkInner(props, ref) {
|
|
9
|
+
const {
|
|
10
|
+
to,
|
|
11
|
+
type = "push",
|
|
12
|
+
replace,
|
|
13
|
+
exact = "include",
|
|
14
|
+
activeClass,
|
|
15
|
+
event = "click",
|
|
16
|
+
tag = "a",
|
|
17
|
+
layerOptions,
|
|
18
|
+
beforeNavigate,
|
|
19
|
+
children,
|
|
20
|
+
className,
|
|
21
|
+
style,
|
|
22
|
+
...rest
|
|
23
|
+
} = props;
|
|
24
|
+
const router = useRouter();
|
|
25
|
+
const linkResolved = useMemo(() => {
|
|
26
|
+
return router.resolveLink({
|
|
27
|
+
to,
|
|
28
|
+
type,
|
|
29
|
+
replace,
|
|
30
|
+
exact,
|
|
31
|
+
activeClass,
|
|
32
|
+
event,
|
|
33
|
+
tag,
|
|
34
|
+
layerOptions,
|
|
35
|
+
beforeNavigate
|
|
36
|
+
});
|
|
37
|
+
}, [
|
|
38
|
+
router,
|
|
39
|
+
to,
|
|
40
|
+
type,
|
|
41
|
+
replace,
|
|
42
|
+
exact,
|
|
43
|
+
activeClass,
|
|
44
|
+
event,
|
|
45
|
+
tag,
|
|
46
|
+
layerOptions,
|
|
47
|
+
beforeNavigate
|
|
48
|
+
]);
|
|
49
|
+
const handleClick = useCallback(
|
|
50
|
+
async (e) => {
|
|
51
|
+
beforeNavigate == null ? void 0 : beforeNavigate(e, "click");
|
|
52
|
+
if (e.defaultPrevented) return;
|
|
53
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
54
|
+
if (e.button !== 0) return;
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
await linkResolved.navigate(e);
|
|
57
|
+
},
|
|
58
|
+
[linkResolved, beforeNavigate]
|
|
59
|
+
);
|
|
60
|
+
const computedClassName = useMemo(() => {
|
|
61
|
+
const classes = [];
|
|
62
|
+
if (linkResolved.attributes.class) {
|
|
63
|
+
classes.push(linkResolved.attributes.class);
|
|
64
|
+
}
|
|
65
|
+
if (className) {
|
|
66
|
+
classes.push(className);
|
|
67
|
+
}
|
|
68
|
+
return classes.join(" ") || void 0;
|
|
69
|
+
}, [linkResolved.attributes.class, className]);
|
|
70
|
+
const elementProps = {
|
|
71
|
+
ref,
|
|
72
|
+
href: linkResolved.attributes.href,
|
|
73
|
+
target: linkResolved.attributes.target,
|
|
74
|
+
rel: linkResolved.attributes.rel,
|
|
75
|
+
className: computedClassName,
|
|
76
|
+
style,
|
|
77
|
+
onClick: handleClick,
|
|
78
|
+
...rest
|
|
79
|
+
};
|
|
80
|
+
return createElement(tag, elementProps, children);
|
|
81
|
+
}
|
|
82
|
+
export const RouterLink = forwardRef(
|
|
83
|
+
RouterLinkInner
|
|
84
|
+
);
|
|
85
|
+
RouterLink.displayName = "RouterLink";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { RouterProviderProps } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* RouterProvider component that provides router context to the React tree.
|
|
4
|
+
* This must wrap your application to enable routing functionality.
|
|
5
|
+
* Uses useSyncExternalStore for optimal React 18+ integration with concurrent features.
|
|
6
|
+
*
|
|
7
|
+
* @param props - Component props
|
|
8
|
+
* @param props.router - Router instance to provide
|
|
9
|
+
* @param props.children - Child components
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* import { Router, RouterMode } from '@esmx/router';
|
|
14
|
+
* import { RouterProvider } from '@esmx/router-react';
|
|
15
|
+
*
|
|
16
|
+
* const routes = [
|
|
17
|
+
* { path: '/', component: Home },
|
|
18
|
+
* { path: '/about', component: About }
|
|
19
|
+
* ];
|
|
20
|
+
*
|
|
21
|
+
* const router = new Router({
|
|
22
|
+
* routes,
|
|
23
|
+
* mode: RouterMode.history
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* function App() {
|
|
27
|
+
* return (
|
|
28
|
+
* <RouterProvider router={router}>
|
|
29
|
+
* <Layout>
|
|
30
|
+
* <RouterView />
|
|
31
|
+
* </Layout>
|
|
32
|
+
* </RouterProvider>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* // SSR usage - initialize router with server URL
|
|
40
|
+
* import { Router, RouterMode } from '@esmx/router';
|
|
41
|
+
* import { RouterProvider } from '@esmx/router-react';
|
|
42
|
+
*
|
|
43
|
+
* const router = new Router({
|
|
44
|
+
* routes,
|
|
45
|
+
* mode: RouterMode.history,
|
|
46
|
+
* base: new URL(serverUrl)
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* function ServerApp() {
|
|
50
|
+
* return (
|
|
51
|
+
* <RouterProvider router={router}>
|
|
52
|
+
* <App />
|
|
53
|
+
* </RouterProvider>
|
|
54
|
+
* );
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export declare function RouterProvider({ router, children }: RouterProviderProps): React.ReactElement;
|
|
59
|
+
export declare namespace RouterProvider {
|
|
60
|
+
var displayName: string;
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { createElement, useCallback, useSyncExternalStore } from "react";
|
|
2
|
+
import { RouterContext } from "./context.mjs";
|
|
3
|
+
export function RouterProvider({
|
|
4
|
+
router,
|
|
5
|
+
children
|
|
6
|
+
}) {
|
|
7
|
+
const subscribe = useCallback(
|
|
8
|
+
(callback) => {
|
|
9
|
+
return router.afterEach(callback);
|
|
10
|
+
},
|
|
11
|
+
[router]
|
|
12
|
+
);
|
|
13
|
+
const getSnapshot = useCallback(() => {
|
|
14
|
+
return router.route;
|
|
15
|
+
}, [router]);
|
|
16
|
+
const getServerSnapshot = useCallback(() => {
|
|
17
|
+
return router.route;
|
|
18
|
+
}, [router]);
|
|
19
|
+
const route = useSyncExternalStore(
|
|
20
|
+
subscribe,
|
|
21
|
+
getSnapshot,
|
|
22
|
+
getServerSnapshot
|
|
23
|
+
);
|
|
24
|
+
const contextValue = {
|
|
25
|
+
router,
|
|
26
|
+
route
|
|
27
|
+
};
|
|
28
|
+
return createElement(
|
|
29
|
+
RouterContext.Provider,
|
|
30
|
+
{ value: contextValue },
|
|
31
|
+
children
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
RouterProvider.displayName = "RouterProvider";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import type { RouterViewProps } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* RouterView component that renders the matched route component.
|
|
5
|
+
* Acts as a placeholder where route components are rendered based on the current route.
|
|
6
|
+
* Supports nested routing with automatic depth tracking.
|
|
7
|
+
*
|
|
8
|
+
* @param props - Component props
|
|
9
|
+
* @param props.fallback - Optional fallback component when no route matches
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // Basic usage
|
|
14
|
+
* import { RouterView } from '@esmx/router-react';
|
|
15
|
+
*
|
|
16
|
+
* function App() {
|
|
17
|
+
* return (
|
|
18
|
+
* <div>
|
|
19
|
+
* <nav>
|
|
20
|
+
* <RouterLink to="/">Home</RouterLink>
|
|
21
|
+
* <RouterLink to="/about">About</RouterLink>
|
|
22
|
+
* </nav>
|
|
23
|
+
* <RouterView />
|
|
24
|
+
* </div>
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* // Nested routing
|
|
32
|
+
* // Routes: [
|
|
33
|
+
* // { path: '/users', component: UsersLayout, children: [
|
|
34
|
+
* // { path: ':id', component: UserProfile }
|
|
35
|
+
* // ]}
|
|
36
|
+
* // ]
|
|
37
|
+
*
|
|
38
|
+
* function UsersLayout() {
|
|
39
|
+
* return (
|
|
40
|
+
* <div>
|
|
41
|
+
* <h1>Users</h1>
|
|
42
|
+
* <RouterView /> // Renders UserProfile for /users/:id
|
|
43
|
+
* </div>
|
|
44
|
+
* );
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* // With fallback
|
|
51
|
+
* import { RouterView } from '@esmx/router-react';
|
|
52
|
+
*
|
|
53
|
+
* function App() {
|
|
54
|
+
* return (
|
|
55
|
+
* <RouterView fallback={<div>Page not found</div>} />
|
|
56
|
+
* );
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function RouterView({ fallback }: RouterViewProps): ReactElement | null;
|
|
61
|
+
export declare namespace RouterView {
|
|
62
|
+
var displayName: string;
|
|
63
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createElement,
|
|
3
|
+
isValidElement,
|
|
4
|
+
useMemo
|
|
5
|
+
} from "react";
|
|
6
|
+
import {
|
|
7
|
+
RouterViewDepthContext,
|
|
8
|
+
useRoute,
|
|
9
|
+
useRouterViewDepth
|
|
10
|
+
} from "./context.mjs";
|
|
11
|
+
import { resolveComponent } from "./util.mjs";
|
|
12
|
+
export function RouterView({ fallback }) {
|
|
13
|
+
const route = useRoute();
|
|
14
|
+
const depth = useRouterViewDepth();
|
|
15
|
+
const matchedRoute = route.matched[depth];
|
|
16
|
+
const Component = useMemo(() => {
|
|
17
|
+
if (!(matchedRoute == null ? void 0 : matchedRoute.component)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return resolveComponent(matchedRoute.component);
|
|
21
|
+
}, [matchedRoute == null ? void 0 : matchedRoute.component]);
|
|
22
|
+
if (!Component) {
|
|
23
|
+
if (fallback) {
|
|
24
|
+
if (isValidElement(fallback)) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
if (typeof fallback === "function") {
|
|
28
|
+
return createElement(fallback);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return createElement(
|
|
35
|
+
RouterViewDepthContext.Provider,
|
|
36
|
+
{ value: depth + 1 },
|
|
37
|
+
createElement(Component, { key: matchedRoute.compilePath })
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
RouterView.displayName = "RouterView";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Route, Router } from '@esmx/router';
|
|
2
|
+
/**
|
|
3
|
+
* Interface for the router context value.
|
|
4
|
+
* Contains the router instance and current route.
|
|
5
|
+
*/
|
|
6
|
+
export interface RouterContextValue {
|
|
7
|
+
/** Router instance for navigation */
|
|
8
|
+
router: Router;
|
|
9
|
+
/** Current route object */
|
|
10
|
+
route: Route;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Props for the RouterProvider component.
|
|
14
|
+
*/
|
|
15
|
+
export interface RouterProviderProps {
|
|
16
|
+
/** Router instance to provide to child components */
|
|
17
|
+
router: Router;
|
|
18
|
+
/** Child components */
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Props for the RouterView component.
|
|
23
|
+
*/
|
|
24
|
+
export interface RouterViewProps {
|
|
25
|
+
/** Optional fallback component to render when no route matches */
|
|
26
|
+
fallback?: React.ComponentType | React.ReactNode;
|
|
27
|
+
}
|
package/dist/types.mjs
ADDED
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { RouterLinkProps, RouterLinkResolved } from '@esmx/router';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to create reactive link helpers for custom navigation components.
|
|
4
|
+
* Returns a resolved link object with attributes, state, and event handlers.
|
|
5
|
+
*
|
|
6
|
+
* This hook is useful when you need to build custom link components
|
|
7
|
+
* with full control over rendering while retaining router functionality.
|
|
8
|
+
*
|
|
9
|
+
* @param props - RouterLink properties
|
|
10
|
+
* @returns Resolved link object with attributes, state, and navigation methods
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* import { useLink } from '@esmx/router-react';
|
|
15
|
+
*
|
|
16
|
+
* function CustomNavButton({ to, children }) {
|
|
17
|
+
* const link = useLink({ to, type: 'push', exact: 'include' });
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <button
|
|
21
|
+
* onClick={(e) => link.navigate(e)}
|
|
22
|
+
* className={link.isActive ? 'active' : ''}
|
|
23
|
+
* disabled={link.isExactActive}
|
|
24
|
+
* >
|
|
25
|
+
* {children}
|
|
26
|
+
* {link.isActive && <span>✓</span>}
|
|
27
|
+
* </button>
|
|
28
|
+
* );
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* // Building a custom navigation card
|
|
35
|
+
* import { useLink } from '@esmx/router-react';
|
|
36
|
+
*
|
|
37
|
+
* interface NavCardProps {
|
|
38
|
+
* to: string;
|
|
39
|
+
* title: string;
|
|
40
|
+
* description: string;
|
|
41
|
+
* icon: React.ReactNode;
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* function NavCard({ to, title, description, icon }: NavCardProps) {
|
|
45
|
+
* const link = useLink({ to });
|
|
46
|
+
*
|
|
47
|
+
* return (
|
|
48
|
+
* <div
|
|
49
|
+
* className={`nav-card ${link.isActive ? 'active' : ''}`}
|
|
50
|
+
* onClick={(e) => link.navigate(e)}
|
|
51
|
+
* role="link"
|
|
52
|
+
* tabIndex={0}
|
|
53
|
+
* >
|
|
54
|
+
* <div className="icon">{icon}</div>
|
|
55
|
+
* <h3>{title}</h3>
|
|
56
|
+
* <p>{description}</p>
|
|
57
|
+
* {link.isExternal && <span className="external-badge">↗</span>}
|
|
58
|
+
* </div>
|
|
59
|
+
* );
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```tsx
|
|
65
|
+
* // Using all resolved properties
|
|
66
|
+
* import { useLink } from '@esmx/router-react';
|
|
67
|
+
*
|
|
68
|
+
* function DebugLink({ to }) {
|
|
69
|
+
* const link = useLink({ to, exact: 'exact' });
|
|
70
|
+
*
|
|
71
|
+
* return (
|
|
72
|
+
* <div>
|
|
73
|
+
* <a
|
|
74
|
+
* href={link.attributes.href}
|
|
75
|
+
* target={link.attributes.target}
|
|
76
|
+
* rel={link.attributes.rel}
|
|
77
|
+
* className={link.attributes.class}
|
|
78
|
+
* onClick={(e) => {
|
|
79
|
+
* e.preventDefault();
|
|
80
|
+
* link.navigate(e);
|
|
81
|
+
* }}
|
|
82
|
+
* >
|
|
83
|
+
* Link Text
|
|
84
|
+
* </a>
|
|
85
|
+
* <pre>
|
|
86
|
+
* isActive: {String(link.isActive)}
|
|
87
|
+
* isExactActive: {String(link.isExactActive)}
|
|
88
|
+
* isExternal: {String(link.isExternal)}
|
|
89
|
+
* type: {link.type}
|
|
90
|
+
* tag: {link.tag}
|
|
91
|
+
* </pre>
|
|
92
|
+
* </div>
|
|
93
|
+
* );
|
|
94
|
+
* }
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export declare function useLink(props: RouterLinkProps): RouterLinkResolved;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useRouter } from "./context.mjs";
|
|
3
|
+
export function useLink(props) {
|
|
4
|
+
const router = useRouter();
|
|
5
|
+
const {
|
|
6
|
+
to,
|
|
7
|
+
type,
|
|
8
|
+
replace,
|
|
9
|
+
exact,
|
|
10
|
+
activeClass,
|
|
11
|
+
event,
|
|
12
|
+
tag,
|
|
13
|
+
layerOptions,
|
|
14
|
+
beforeNavigate
|
|
15
|
+
} = props;
|
|
16
|
+
return useMemo(() => {
|
|
17
|
+
return router.resolveLink({
|
|
18
|
+
to,
|
|
19
|
+
type,
|
|
20
|
+
replace,
|
|
21
|
+
exact,
|
|
22
|
+
activeClass,
|
|
23
|
+
event,
|
|
24
|
+
tag,
|
|
25
|
+
layerOptions,
|
|
26
|
+
beforeNavigate
|
|
27
|
+
});
|
|
28
|
+
}, [
|
|
29
|
+
router,
|
|
30
|
+
to,
|
|
31
|
+
type,
|
|
32
|
+
replace,
|
|
33
|
+
exact,
|
|
34
|
+
activeClass,
|
|
35
|
+
event,
|
|
36
|
+
tag,
|
|
37
|
+
layerOptions,
|
|
38
|
+
beforeNavigate
|
|
39
|
+
]);
|
|
40
|
+
}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if a value is an ES module.
|
|
3
|
+
* @param obj - The object to check
|
|
4
|
+
* @returns True if the object is an ES module
|
|
5
|
+
*/
|
|
6
|
+
export declare function isESModule(obj: unknown): obj is Record<string | symbol, any>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a component from potentially wrapped module format.
|
|
9
|
+
* Handles ES modules with default exports.
|
|
10
|
+
* @param component - The component to resolve
|
|
11
|
+
* @returns The resolved component
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveComponent(component: unknown): unknown;
|
package/dist/util.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isESModule(obj) {
|
|
2
|
+
if (!obj || typeof obj !== "object") return false;
|
|
3
|
+
const module = obj;
|
|
4
|
+
return Boolean(module.__esModule) || module[Symbol.toStringTag] === "Module";
|
|
5
|
+
}
|
|
6
|
+
export function resolveComponent(component) {
|
|
7
|
+
if (!component) return null;
|
|
8
|
+
if (isESModule(component)) {
|
|
9
|
+
return component.default || component;
|
|
10
|
+
}
|
|
11
|
+
if (component && typeof component === "object" && !Array.isArray(component) && "default" in component && Object.keys(component).length === 1) {
|
|
12
|
+
return component.default;
|
|
13
|
+
}
|
|
14
|
+
return component;
|
|
15
|
+
}
|