@bromscandium/router 1.0.0
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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/components.d.ts +129 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +287 -0
- package/dist/components.js.map +1 -0
- package/dist/hooks.d.ts +136 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +167 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/router.d.ts +159 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +282 -0
- package/dist/router.js.map +1 -0
- package/package.json +42 -0
- package/src/components.tsx +411 -0
- package/src/hooks.ts +180 -0
- package/src/index.ts +31 -0
- package/src/router.ts +448 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bromscandium/router",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "BromiumJS router with file-based routing support",
|
|
5
|
+
"author": "bromscandium",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/bromscandium/bromiumjs.git",
|
|
10
|
+
"directory": "packages/router"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/bromscandium/bromiumjs#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/bromscandium/bromiumjs/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["bromium", "bromiumjs", "router", "file-based-routing", "spa"],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"dev": "tsc --watch"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@bromscandium/core": "^1.0.0",
|
|
37
|
+
"@bromscandium/runtime": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"typescript": "^5.9.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router components: Link, RouterView, Redirect, and Navigate.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {ref} from '@bromscandium/core';
|
|
7
|
+
import {jsx, VNode} from '@bromscandium/runtime';
|
|
8
|
+
import {useRoute, useRouter} from './hooks.js';
|
|
9
|
+
import {NavigationTarget} from './router.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Props for the Link component.
|
|
13
|
+
*/
|
|
14
|
+
interface LinkProps {
|
|
15
|
+
/** The target path or navigation object */
|
|
16
|
+
to: string | NavigationTarget;
|
|
17
|
+
/** Child elements to render inside the link */
|
|
18
|
+
children?: any;
|
|
19
|
+
/** CSS class name */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Class name to add when route is active (includes nested) */
|
|
22
|
+
activeClassName?: string;
|
|
23
|
+
/** Class name to add when route exactly matches */
|
|
24
|
+
exactActiveClassName?: string;
|
|
25
|
+
/** If true, replace current history entry instead of pushing */
|
|
26
|
+
replace?: boolean;
|
|
27
|
+
/** HTML target attribute (e.g., "_blank") */
|
|
28
|
+
target?: string;
|
|
29
|
+
/** HTML rel attribute for external links */
|
|
30
|
+
rel?: string;
|
|
31
|
+
/** Additional click handler */
|
|
32
|
+
onClick?: (e: MouseEvent) => void;
|
|
33
|
+
/** Additional attributes passed to the anchor element */
|
|
34
|
+
[key: string]: any;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A navigation link component that integrates with the router.
|
|
39
|
+
* Handles click events to perform client-side navigation.
|
|
40
|
+
*
|
|
41
|
+
* @param props - Link component props
|
|
42
|
+
* @returns An anchor element VNode
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```tsx
|
|
46
|
+
* <Link to="/about">About</Link>
|
|
47
|
+
*
|
|
48
|
+
* <Link to="/dashboard" activeClassName="active">Dashboard</Link>
|
|
49
|
+
*
|
|
50
|
+
* <Link to={{ path: '/user', query: { id: '123' } }}>User</Link>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function Link(props: LinkProps): VNode {
|
|
54
|
+
const router = useRouter();
|
|
55
|
+
const route = useRoute();
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
to,
|
|
59
|
+
children,
|
|
60
|
+
className = '',
|
|
61
|
+
activeClassName = '',
|
|
62
|
+
exactActiveClassName = '',
|
|
63
|
+
replace = false,
|
|
64
|
+
target,
|
|
65
|
+
rel,
|
|
66
|
+
onClick,
|
|
67
|
+
...rest
|
|
68
|
+
} = props;
|
|
69
|
+
|
|
70
|
+
const href = typeof to === 'string' ? to : (to.path || '/');
|
|
71
|
+
|
|
72
|
+
const currentPath = route.value.path;
|
|
73
|
+
const base = router.base || '';
|
|
74
|
+
const comparePath = base ? href.replace(new RegExp(`^${base}`), '') || '/' : href;
|
|
75
|
+
|
|
76
|
+
const isExactActive = currentPath === comparePath;
|
|
77
|
+
const isActive = isExactActive || currentPath.startsWith(comparePath + '/');
|
|
78
|
+
|
|
79
|
+
let finalClassName = className;
|
|
80
|
+
if (isActive && activeClassName) {
|
|
81
|
+
finalClassName = `${finalClassName} ${activeClassName}`.trim();
|
|
82
|
+
}
|
|
83
|
+
if (isExactActive && exactActiveClassName) {
|
|
84
|
+
finalClassName = `${finalClassName} ${exactActiveClassName}`.trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function handleClick(e: MouseEvent) {
|
|
88
|
+
onClick?.(e);
|
|
89
|
+
|
|
90
|
+
if (e.defaultPrevented) return;
|
|
91
|
+
|
|
92
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
93
|
+
|
|
94
|
+
if (target) return;
|
|
95
|
+
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
|
|
98
|
+
if (replace) {
|
|
99
|
+
router.replace(to);
|
|
100
|
+
} else {
|
|
101
|
+
router.push(to);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return jsx('a', {
|
|
106
|
+
href,
|
|
107
|
+
className: finalClassName || undefined,
|
|
108
|
+
target,
|
|
109
|
+
rel,
|
|
110
|
+
onClick: handleClick,
|
|
111
|
+
children,
|
|
112
|
+
...rest,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Props for the RouterView component.
|
|
118
|
+
*/
|
|
119
|
+
interface RouterViewProps {
|
|
120
|
+
/** The nesting depth for nested RouterViews (usually auto-detected) */
|
|
121
|
+
depth?: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let routerViewDepth = 0;
|
|
125
|
+
|
|
126
|
+
const componentCache = new Map<string, any>();
|
|
127
|
+
|
|
128
|
+
const layoutCache = new Map<string, any>();
|
|
129
|
+
|
|
130
|
+
const pendingLoads = new Map<string, Promise<any>>();
|
|
131
|
+
|
|
132
|
+
const renderVersion = ref(0);
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Renders the component for the current matched route.
|
|
136
|
+
* Handles lazy loading and layout wrapping automatically.
|
|
137
|
+
*
|
|
138
|
+
* @param props - RouterView component props
|
|
139
|
+
* @returns The rendered route component or null
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```tsx
|
|
143
|
+
* function App() {
|
|
144
|
+
* return (
|
|
145
|
+
* <div>
|
|
146
|
+
* <nav>...</nav>
|
|
147
|
+
* <RouterView />
|
|
148
|
+
* </div>
|
|
149
|
+
* );
|
|
150
|
+
* }
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
export function RouterView(props: RouterViewProps): VNode | null {
|
|
154
|
+
const { depth: explicitDepth } = props;
|
|
155
|
+
const route = useRoute();
|
|
156
|
+
|
|
157
|
+
void renderVersion.value;
|
|
158
|
+
|
|
159
|
+
const currentDepth = explicitDepth ?? routerViewDepth;
|
|
160
|
+
const matched = route.value.matched[currentDepth];
|
|
161
|
+
|
|
162
|
+
if (!matched) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const routePath = matched.path;
|
|
167
|
+
const routeConfig = matched.route;
|
|
168
|
+
|
|
169
|
+
if (routeConfig.layout) {
|
|
170
|
+
const layoutKey = `layout:${routePath}`;
|
|
171
|
+
return loadAndRenderLayoutWithPage(
|
|
172
|
+
routeConfig.layout,
|
|
173
|
+
layoutKey,
|
|
174
|
+
routeConfig.component,
|
|
175
|
+
routePath,
|
|
176
|
+
route.value.fullPath,
|
|
177
|
+
currentDepth
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return loadAndRenderComponent(
|
|
182
|
+
routeConfig.component,
|
|
183
|
+
routePath,
|
|
184
|
+
route.value.fullPath,
|
|
185
|
+
currentDepth
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function loadAndRenderComponent(
|
|
190
|
+
componentModule: () => Promise<{ default: any }> | { default: any },
|
|
191
|
+
routePath: string,
|
|
192
|
+
fullPath: string,
|
|
193
|
+
currentDepth: number
|
|
194
|
+
): VNode | null {
|
|
195
|
+
if (componentCache.has(routePath)) {
|
|
196
|
+
const Component = componentCache.get(routePath);
|
|
197
|
+
routerViewDepth = currentDepth + 1;
|
|
198
|
+
try {
|
|
199
|
+
return jsx(Component, {
|
|
200
|
+
key: fullPath,
|
|
201
|
+
});
|
|
202
|
+
} finally {
|
|
203
|
+
routerViewDepth = currentDepth;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (typeof componentModule === 'function') {
|
|
208
|
+
if (!pendingLoads.has(routePath)) {
|
|
209
|
+
const loadPromise = componentModule();
|
|
210
|
+
|
|
211
|
+
if (loadPromise && typeof (loadPromise as any).then === 'function') {
|
|
212
|
+
pendingLoads.set(routePath, loadPromise as Promise<any>);
|
|
213
|
+
|
|
214
|
+
(loadPromise as Promise<any>).then((result) => {
|
|
215
|
+
const Component = result.default || result;
|
|
216
|
+
componentCache.set(routePath, Component);
|
|
217
|
+
pendingLoads.delete(routePath);
|
|
218
|
+
renderVersion.value++;
|
|
219
|
+
}).catch((error) => {
|
|
220
|
+
console.error('Failed to load route component:', error);
|
|
221
|
+
pendingLoads.delete(routePath);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return jsx('div', {
|
|
225
|
+
className: 'router-loading',
|
|
226
|
+
children: '',
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
const Component = (loadPromise as any).default || loadPromise;
|
|
230
|
+
componentCache.set(routePath, Component);
|
|
231
|
+
|
|
232
|
+
routerViewDepth = currentDepth + 1;
|
|
233
|
+
try {
|
|
234
|
+
return jsx(Component, {
|
|
235
|
+
key: fullPath,
|
|
236
|
+
});
|
|
237
|
+
} finally {
|
|
238
|
+
routerViewDepth = currentDepth;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
return jsx('div', {
|
|
243
|
+
className: 'router-loading',
|
|
244
|
+
children: '',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
const Component = (componentModule as any).default || componentModule;
|
|
249
|
+
componentCache.set(routePath, Component);
|
|
250
|
+
|
|
251
|
+
routerViewDepth = currentDepth + 1;
|
|
252
|
+
try {
|
|
253
|
+
return jsx(Component, {
|
|
254
|
+
key: fullPath,
|
|
255
|
+
});
|
|
256
|
+
} finally {
|
|
257
|
+
routerViewDepth = currentDepth;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function loadAndRenderLayoutWithPage(
|
|
265
|
+
layoutModule: () => Promise<{ default: any }> | { default: any },
|
|
266
|
+
layoutKey: string,
|
|
267
|
+
pageModule: () => Promise<{ default: any }> | { default: any },
|
|
268
|
+
routePath: string,
|
|
269
|
+
fullPath: string,
|
|
270
|
+
currentDepth: number
|
|
271
|
+
): VNode | null {
|
|
272
|
+
const layoutLoaded = layoutCache.has(layoutKey);
|
|
273
|
+
const pageLoaded = componentCache.has(routePath);
|
|
274
|
+
|
|
275
|
+
if (layoutLoaded && pageLoaded) {
|
|
276
|
+
const Layout = layoutCache.get(layoutKey);
|
|
277
|
+
const PageComponent = componentCache.get(routePath);
|
|
278
|
+
|
|
279
|
+
routerViewDepth = currentDepth + 1;
|
|
280
|
+
try {
|
|
281
|
+
const pageElement = jsx(PageComponent, { key: fullPath });
|
|
282
|
+
return jsx(Layout, { children: pageElement });
|
|
283
|
+
} finally {
|
|
284
|
+
routerViewDepth = currentDepth;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!layoutLoaded && typeof layoutModule === 'function' && !pendingLoads.has(layoutKey)) {
|
|
289
|
+
const loadPromise = layoutModule();
|
|
290
|
+
|
|
291
|
+
if (loadPromise && typeof (loadPromise as any).then === 'function') {
|
|
292
|
+
pendingLoads.set(layoutKey, loadPromise as Promise<any>);
|
|
293
|
+
|
|
294
|
+
(loadPromise as Promise<any>).then((result) => {
|
|
295
|
+
const Layout = result.default || result;
|
|
296
|
+
layoutCache.set(layoutKey, Layout);
|
|
297
|
+
pendingLoads.delete(layoutKey);
|
|
298
|
+
renderVersion.value++;
|
|
299
|
+
}).catch((error) => {
|
|
300
|
+
console.error('Failed to load layout:', error);
|
|
301
|
+
pendingLoads.delete(layoutKey);
|
|
302
|
+
});
|
|
303
|
+
} else {
|
|
304
|
+
const Layout = (loadPromise as any).default || loadPromise;
|
|
305
|
+
layoutCache.set(layoutKey, Layout);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (!pageLoaded && typeof pageModule === 'function' && !pendingLoads.has(routePath)) {
|
|
310
|
+
const loadPromise = pageModule();
|
|
311
|
+
|
|
312
|
+
if (loadPromise && typeof (loadPromise as any).then === 'function') {
|
|
313
|
+
pendingLoads.set(routePath, loadPromise as Promise<any>);
|
|
314
|
+
|
|
315
|
+
(loadPromise as Promise<any>).then((result) => {
|
|
316
|
+
const PageComponent = result.default || result;
|
|
317
|
+
componentCache.set(routePath, PageComponent);
|
|
318
|
+
pendingLoads.delete(routePath);
|
|
319
|
+
renderVersion.value++;
|
|
320
|
+
}).catch((error) => {
|
|
321
|
+
console.error('Failed to load page component:', error);
|
|
322
|
+
pendingLoads.delete(routePath);
|
|
323
|
+
});
|
|
324
|
+
} else {
|
|
325
|
+
const PageComponent = (loadPromise as any).default || loadPromise;
|
|
326
|
+
componentCache.set(routePath, PageComponent);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return jsx('div', {
|
|
331
|
+
className: 'router-loading',
|
|
332
|
+
children: '',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Props for the Redirect component.
|
|
338
|
+
*/
|
|
339
|
+
interface RedirectProps {
|
|
340
|
+
/** The target path or navigation object */
|
|
341
|
+
to: string | NavigationTarget;
|
|
342
|
+
/** If true, replace current history entry (default: true) */
|
|
343
|
+
replace?: boolean;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* A component that performs a redirect when rendered.
|
|
348
|
+
* Useful for redirecting from one route to another.
|
|
349
|
+
*
|
|
350
|
+
* @param props - Redirect component props
|
|
351
|
+
* @returns null (performs navigation as side effect)
|
|
352
|
+
*
|
|
353
|
+
* @example
|
|
354
|
+
* ```tsx
|
|
355
|
+
* // In a route component
|
|
356
|
+
* if (!isAuthenticated) {
|
|
357
|
+
* return <Redirect to="/login" />;
|
|
358
|
+
* }
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
export function Redirect(props: RedirectProps): null {
|
|
362
|
+
const router = useRouter();
|
|
363
|
+
const { to, replace = true } = props;
|
|
364
|
+
|
|
365
|
+
if (replace) {
|
|
366
|
+
router.replace(to);
|
|
367
|
+
} else {
|
|
368
|
+
router.push(to);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Props for the Navigate component.
|
|
376
|
+
*/
|
|
377
|
+
interface NavigateProps {
|
|
378
|
+
/** The target path or navigation object */
|
|
379
|
+
to: string | NavigationTarget;
|
|
380
|
+
/** If true, replace current history entry instead of pushing */
|
|
381
|
+
replace?: boolean;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* A component that performs navigation when rendered.
|
|
386
|
+
* Alternative to using the router imperatively.
|
|
387
|
+
*
|
|
388
|
+
* @param props - Navigate component props
|
|
389
|
+
* @returns null (performs navigation as side effect)
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```tsx
|
|
393
|
+
* function AfterSubmit({ success }) {
|
|
394
|
+
* if (success) {
|
|
395
|
+
* return <Navigate to="/success" />;
|
|
396
|
+
* }
|
|
397
|
+
* return <div>Form</div>;
|
|
398
|
+
* }
|
|
399
|
+
* ```
|
|
400
|
+
*/
|
|
401
|
+
export function Navigate(props: NavigateProps): null {
|
|
402
|
+
const router = useRouter();
|
|
403
|
+
|
|
404
|
+
if (props.replace) {
|
|
405
|
+
router.replace(props.to);
|
|
406
|
+
} else {
|
|
407
|
+
router.push(props.to);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return null;
|
|
411
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router hooks for components.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { computed, ComputedRef, Ref } from '@bromscandium/core';
|
|
7
|
+
import { getRouter, Router, RouteLocation } from './router.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the router instance.
|
|
11
|
+
* Must be called within a component where a router has been created.
|
|
12
|
+
*
|
|
13
|
+
* @returns The router instance
|
|
14
|
+
* @throws Error if called outside of router context
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* function MyComponent() {
|
|
19
|
+
* const router = useRouter();
|
|
20
|
+
*
|
|
21
|
+
* function handleClick() {
|
|
22
|
+
* router.push('/dashboard');
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* return <button onClick={handleClick}>Go to Dashboard</button>;
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useRouter(): Router {
|
|
30
|
+
const router = getRouter();
|
|
31
|
+
if (!router) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
'useRouter() called outside of router context. ' +
|
|
34
|
+
'Make sure you have created a router with createRouter() and mounted it.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return router;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Returns a reactive reference to the current route location.
|
|
42
|
+
*
|
|
43
|
+
* @returns A ref containing the current RouteLocation
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* function Breadcrumb() {
|
|
48
|
+
* const route = useRoute();
|
|
49
|
+
*
|
|
50
|
+
* return <span>Current path: {route.value.path}</span>;
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function useRoute(): Ref<RouteLocation> {
|
|
55
|
+
const router = useRouter();
|
|
56
|
+
return router.currentRoute;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns a computed ref of the current route parameters.
|
|
61
|
+
*
|
|
62
|
+
* @returns A computed ref containing route params
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* // For route /users/:id
|
|
67
|
+
* function UserProfile() {
|
|
68
|
+
* const params = useParams();
|
|
69
|
+
*
|
|
70
|
+
* return <div>User ID: {params.value.id}</div>;
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export function useParams(): ComputedRef<Record<string, string>> {
|
|
75
|
+
const route = useRoute();
|
|
76
|
+
return computed(() => route.value.params);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns a computed ref of the current query string parameters.
|
|
81
|
+
*
|
|
82
|
+
* @returns A computed ref containing query params
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* // For URL /search?q=hello
|
|
87
|
+
* function SearchResults() {
|
|
88
|
+
* const query = useQuery();
|
|
89
|
+
*
|
|
90
|
+
* return <div>Searching for: {query.value.q}</div>;
|
|
91
|
+
* }
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function useQuery(): ComputedRef<Record<string, string>> {
|
|
95
|
+
const route = useRoute();
|
|
96
|
+
return computed(() => route.value.query);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Returns a computed ref of the merged metadata from all matched routes.
|
|
101
|
+
*
|
|
102
|
+
* @returns A computed ref containing route metadata
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* function PageTitle() {
|
|
107
|
+
* const meta = useRouteMeta();
|
|
108
|
+
*
|
|
109
|
+
* return <title>{meta.value.title || 'My App'}</title>;
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export function useRouteMeta(): ComputedRef<Record<string, any>> {
|
|
114
|
+
const route = useRoute();
|
|
115
|
+
return computed(() => route.value.meta);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Returns navigation helper functions from the router.
|
|
120
|
+
*
|
|
121
|
+
* @returns An object with push, replace, back, forward, and go methods
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* function Navigation() {
|
|
126
|
+
* const { push, back } = useNavigate();
|
|
127
|
+
*
|
|
128
|
+
* return (
|
|
129
|
+
* <>
|
|
130
|
+
* <button onClick={back}>Back</button>
|
|
131
|
+
* <button onClick={() => push('/home')}>Home</button>
|
|
132
|
+
* </>
|
|
133
|
+
* );
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function useNavigate() {
|
|
138
|
+
const router = useRouter();
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
push: router.push,
|
|
142
|
+
replace: router.replace,
|
|
143
|
+
back: router.back,
|
|
144
|
+
forward: router.forward,
|
|
145
|
+
go: router.go,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns a computed boolean indicating if the current route matches a path.
|
|
151
|
+
*
|
|
152
|
+
* @param path - A string path or RegExp to match against
|
|
153
|
+
* @returns A computed ref that is true when the route matches
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* function NavLink({ to, children }) {
|
|
158
|
+
* const isActive = useRouteMatch(to);
|
|
159
|
+
*
|
|
160
|
+
* return (
|
|
161
|
+
* <a href={to} className={isActive.value ? 'active' : ''}>
|
|
162
|
+
* {children}
|
|
163
|
+
* </a>
|
|
164
|
+
* );
|
|
165
|
+
* }
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function useRouteMatch(path: string | RegExp): ComputedRef<boolean> {
|
|
169
|
+
const route = useRoute();
|
|
170
|
+
|
|
171
|
+
return computed(() => {
|
|
172
|
+
const currentPath = route.value.path;
|
|
173
|
+
|
|
174
|
+
if (typeof path === 'string') {
|
|
175
|
+
return currentPath === path || currentPath.startsWith(path + '/');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return path.test(currentPath);
|
|
179
|
+
});
|
|
180
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Router package exports
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
createRouter,
|
|
5
|
+
getRouter,
|
|
6
|
+
setRouter,
|
|
7
|
+
type Router,
|
|
8
|
+
type Route,
|
|
9
|
+
type RouteLocation,
|
|
10
|
+
type MatchedRoute,
|
|
11
|
+
type NavigationTarget,
|
|
12
|
+
type NavigationGuard,
|
|
13
|
+
type NavigationHook,
|
|
14
|
+
} from './router.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
useRouter,
|
|
18
|
+
useRoute,
|
|
19
|
+
useParams,
|
|
20
|
+
useQuery,
|
|
21
|
+
useRouteMeta,
|
|
22
|
+
useNavigate,
|
|
23
|
+
useRouteMatch,
|
|
24
|
+
} from './hooks.js';
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
Link,
|
|
28
|
+
RouterView,
|
|
29
|
+
Redirect,
|
|
30
|
+
Navigate,
|
|
31
|
+
} from './components.js';
|