@iyulab/router 0.1.0 → 0.2.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.
- package/README.md +299 -70
- package/dist/main.cjs.js +594 -32595
- package/dist/main.d.ts +208 -205
- package/dist/main.es.js +595 -32597
- package/package.json +2 -2
- package/dist/main.umd.js +0 -32729
package/README.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
# @iyulab/router
|
|
2
2
|
|
|
3
|
-
A modern client-side router for web applications with support for Lit and React components.
|
|
3
|
+
A modern, lightweight client-side router for web applications with support for both Lit and React components.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- 🚀 Modern URLPattern-based routing
|
|
8
|
-
- 🔧 Support
|
|
9
|
-
- 📱 Client-side
|
|
10
|
-
- 🎯 Nested
|
|
11
|
-
- ⚡ Async
|
|
12
|
-
- 🔗 Smart
|
|
13
|
-
- 📊 Route progress
|
|
14
|
-
- 🎨 Flexible
|
|
15
|
-
- 🌐 Global route information
|
|
7
|
+
- 🚀 **Modern URLPattern-based routing** - Uses native URLPattern API for powerful path matching
|
|
8
|
+
- 🔧 **Dual Framework Support** - Works with both Lit and React components
|
|
9
|
+
- 📱 **Client-side Navigation** - History API integration with browser back/forward support
|
|
10
|
+
- 🎯 **Nested Routing** - Support for deeply nested route hierarchies
|
|
11
|
+
- ⚡ **Async Data Loading** - Built-in loader functions for data fetching
|
|
12
|
+
- 🔗 **Smart Link Component** - Automatic external link detection and handling
|
|
13
|
+
- 📊 **Route Events** - Track navigation progress and handle errors
|
|
14
|
+
- 🎨 **Flexible Outlet System** - Dynamic component rendering with lifecycle management
|
|
15
|
+
- 🌐 **Global Route Access** - Access current route information anywhere via `window.route`
|
|
16
|
+
- 🔄 **Force Re-rendering** - Control component re-rendering on route changes
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -20,7 +21,7 @@ A modern client-side router for web applications with support for Lit and React
|
|
|
20
21
|
npm install @iyulab/router
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
##
|
|
24
|
+
## Quick Start
|
|
24
25
|
|
|
25
26
|
### Basic Setup
|
|
26
27
|
|
|
@@ -43,20 +44,20 @@ const router = new Router({
|
|
|
43
44
|
return response.json();
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
|
-
]
|
|
47
|
-
notfound: 'not-found-page'
|
|
47
|
+
]
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
//
|
|
51
|
-
router.
|
|
50
|
+
// Start routing
|
|
51
|
+
router.go(window.location.href);
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
## Usage Examples
|
|
55
|
+
|
|
54
56
|
### Using with Lit Components
|
|
55
57
|
|
|
56
58
|
```typescript
|
|
57
59
|
import { LitElement, html } from 'lit';
|
|
58
60
|
import { customElement } from 'lit/decorators.js';
|
|
59
|
-
import { UOutlet, ULink } from '@iyulab/router';
|
|
60
61
|
|
|
61
62
|
@customElement('app-root')
|
|
62
63
|
export class AppRoot extends LitElement {
|
|
@@ -65,7 +66,7 @@ export class AppRoot extends LitElement {
|
|
|
65
66
|
<nav>
|
|
66
67
|
<u-link href="/">Home</u-link>
|
|
67
68
|
<u-link href="/about">About</u-link>
|
|
68
|
-
<u-link href="/user/123">User</u-link>
|
|
69
|
+
<u-link href="/user/123">User Profile</u-link>
|
|
69
70
|
</nav>
|
|
70
71
|
<main>
|
|
71
72
|
<u-outlet></u-outlet>
|
|
@@ -87,7 +88,7 @@ export function AppRoot() {
|
|
|
87
88
|
<nav>
|
|
88
89
|
<Link href="/">Home</Link>
|
|
89
90
|
<Link href="/about">About</Link>
|
|
90
|
-
<Link href="/user/123">User</Link>
|
|
91
|
+
<Link href="/user/123">User Profile</Link>
|
|
91
92
|
</nav>
|
|
92
93
|
<main>
|
|
93
94
|
<Outlet />
|
|
@@ -97,7 +98,9 @@ export function AppRoot() {
|
|
|
97
98
|
}
|
|
98
99
|
```
|
|
99
100
|
|
|
100
|
-
###
|
|
101
|
+
### Advanced Routing Features
|
|
102
|
+
|
|
103
|
+
#### Nested Routes with Layouts
|
|
101
104
|
|
|
102
105
|
```typescript
|
|
103
106
|
const routes = [
|
|
@@ -106,7 +109,7 @@ const routes = [
|
|
|
106
109
|
element: 'dashboard-layout',
|
|
107
110
|
children: [
|
|
108
111
|
{
|
|
109
|
-
index: true,
|
|
112
|
+
index: true, // Matches /dashboard exactly
|
|
110
113
|
element: 'dashboard-home'
|
|
111
114
|
},
|
|
112
115
|
{
|
|
@@ -116,71 +119,123 @@ const routes = [
|
|
|
116
119
|
{
|
|
117
120
|
path: 'profile',
|
|
118
121
|
component: ProfileComponent
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
path: 'users/:id',
|
|
125
|
+
component: UserDetailComponent,
|
|
126
|
+
loader: async (routeInfo) => {
|
|
127
|
+
const userId = routeInfo.params.id;
|
|
128
|
+
return await fetchUserData(userId);
|
|
129
|
+
}
|
|
119
130
|
}
|
|
120
131
|
]
|
|
121
132
|
}
|
|
122
133
|
];
|
|
123
134
|
```
|
|
124
135
|
|
|
125
|
-
|
|
136
|
+
#### Route Data Loading
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Routes with async data loading
|
|
140
|
+
{
|
|
141
|
+
path: '/posts/:slug',
|
|
142
|
+
component: PostComponent,
|
|
143
|
+
loader: async (routeInfo) => {
|
|
144
|
+
const { slug } = routeInfo.params;
|
|
145
|
+
const post = await fetch(`/api/posts/${slug}`).then(r => r.json());
|
|
146
|
+
return { post };
|
|
147
|
+
},
|
|
148
|
+
title: 'Blog Post' // Sets document title
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### Force Re-rendering Control
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
const routes = [
|
|
156
|
+
{
|
|
157
|
+
path: '/live-data',
|
|
158
|
+
component: LiveDataComponent,
|
|
159
|
+
force: true // Always re-render on navigation
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
path: '/cached-content',
|
|
163
|
+
component: CachedComponent,
|
|
164
|
+
force: false // Reuse existing component instance
|
|
165
|
+
}
|
|
166
|
+
];
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Global Route Information Access
|
|
126
170
|
|
|
127
|
-
|
|
171
|
+
Access current route information anywhere in your application:
|
|
128
172
|
|
|
129
173
|
```typescript
|
|
130
|
-
//
|
|
174
|
+
// Available globally on window.route
|
|
131
175
|
console.log('Current path:', window.route.pathname);
|
|
132
176
|
console.log('Route params:', window.route.params);
|
|
133
177
|
console.log('Query params:', window.route.search);
|
|
134
|
-
console.log('
|
|
178
|
+
console.log('Loaded data:', window.route.data);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Lit Component Route Integration
|
|
135
182
|
|
|
136
|
-
|
|
183
|
+
```typescript
|
|
137
184
|
import { LitElement, html } from 'lit';
|
|
138
|
-
import { customElement } from 'lit/decorators.js';
|
|
185
|
+
import { customElement, state } from 'lit/decorators.js';
|
|
186
|
+
|
|
187
|
+
@customElement('route-aware-component')
|
|
188
|
+
export class RouteAwareComponent extends LitElement {
|
|
189
|
+
@state() private routeInfo = window.route;
|
|
139
190
|
|
|
140
|
-
@customElement('my-component')
|
|
141
|
-
export class MyComponent extends LitElement {
|
|
142
191
|
connectedCallback() {
|
|
143
192
|
super.connectedCallback();
|
|
144
193
|
// Listen for route changes
|
|
145
|
-
|
|
194
|
+
window.addEventListener('route-end', this.handleRouteChange);
|
|
146
195
|
}
|
|
147
196
|
|
|
148
197
|
disconnectedCallback() {
|
|
149
|
-
|
|
198
|
+
window.removeEventListener('route-end', this.handleRouteChange);
|
|
150
199
|
super.disconnectedCallback();
|
|
151
200
|
}
|
|
152
201
|
|
|
153
|
-
private
|
|
154
|
-
this.
|
|
202
|
+
private handleRouteChange = (event: CustomEvent) => {
|
|
203
|
+
this.routeInfo = event.detail;
|
|
204
|
+
this.requestUpdate();
|
|
155
205
|
};
|
|
156
206
|
|
|
157
207
|
render() {
|
|
158
208
|
return html`
|
|
159
|
-
<div>Current path: ${
|
|
160
|
-
<div>
|
|
209
|
+
<div>Current path: ${this.routeInfo?.pathname}</div>
|
|
210
|
+
<div>User ID: ${this.routeInfo?.params?.id}</div>
|
|
211
|
+
<div>Data: ${JSON.stringify(this.routeInfo?.data)}</div>
|
|
161
212
|
`;
|
|
162
213
|
}
|
|
163
214
|
}
|
|
215
|
+
```
|
|
164
216
|
|
|
165
|
-
|
|
217
|
+
#### React Component Route Integration
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
166
220
|
import React, { useState, useEffect } from 'react';
|
|
167
221
|
|
|
168
|
-
export function
|
|
222
|
+
export function RouteAwareComponent() {
|
|
169
223
|
const [routeInfo, setRouteInfo] = useState(window.route);
|
|
170
224
|
|
|
171
225
|
useEffect(() => {
|
|
172
|
-
const handleRouteChange = () => {
|
|
173
|
-
setRouteInfo(
|
|
226
|
+
const handleRouteChange = (event) => {
|
|
227
|
+
setRouteInfo(event.detail);
|
|
174
228
|
};
|
|
175
229
|
|
|
176
|
-
|
|
177
|
-
return () =>
|
|
230
|
+
window.addEventListener('route-end', handleRouteChange);
|
|
231
|
+
return () => window.removeEventListener('route-end', handleRouteChange);
|
|
178
232
|
}, []);
|
|
179
233
|
|
|
180
234
|
return (
|
|
181
235
|
<div>
|
|
182
236
|
<div>Current path: {routeInfo?.pathname}</div>
|
|
183
|
-
<div>
|
|
237
|
+
<div>User ID: {routeInfo?.params?.id}</div>
|
|
238
|
+
<div>Data: {JSON.stringify(routeInfo?.data)}</div>
|
|
184
239
|
</div>
|
|
185
240
|
);
|
|
186
241
|
}
|
|
@@ -188,53 +243,227 @@ export function MyReactComponent() {
|
|
|
188
243
|
|
|
189
244
|
## API Reference
|
|
190
245
|
|
|
191
|
-
### Router
|
|
246
|
+
### Router Class
|
|
247
|
+
|
|
248
|
+
#### Constructor
|
|
192
249
|
|
|
193
|
-
|
|
250
|
+
```typescript
|
|
251
|
+
new Router(config: RouterConfig)
|
|
252
|
+
```
|
|
194
253
|
|
|
195
|
-
|
|
196
|
-
- `
|
|
197
|
-
- `
|
|
198
|
-
- `
|
|
254
|
+
**RouterConfig:**
|
|
255
|
+
- `root: HTMLElement` - Root element where router renders components
|
|
256
|
+
- `basepath?: string` - Base path for all routes (default: '/')
|
|
257
|
+
- `routes: RouteConfig[]` - Array of route configurations
|
|
199
258
|
|
|
200
259
|
#### Methods
|
|
201
260
|
|
|
202
|
-
- `
|
|
203
|
-
- `
|
|
204
|
-
- `
|
|
205
|
-
- `goBase()`: Navigate to the base path
|
|
261
|
+
- `go(href: string): Promise<void>` - Navigate to specified path
|
|
262
|
+
- `get basepath(): string` - Get current base path
|
|
263
|
+
- `get routeInfo(): RouteInfo | undefined` - Get current route information
|
|
206
264
|
|
|
207
265
|
### Route Configuration
|
|
208
266
|
|
|
209
267
|
```typescript
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
268
|
+
interface RouteConfig {
|
|
269
|
+
id?: string; // Unique route identifier (auto-generated)
|
|
270
|
+
path?: string; // URL pattern (URLPattern syntax)
|
|
271
|
+
index?: boolean; // Index route (matches parent exactly)
|
|
272
|
+
title?: string; // Document title
|
|
273
|
+
element?: typeof LitElement | string; // Lit element class or tag name
|
|
274
|
+
component?: ComponentType; // React component
|
|
275
|
+
loader?: (info: RouteInfo) => Promise<any>; // Async data loader
|
|
276
|
+
children?: RouteConfig[]; // Nested routes
|
|
277
|
+
force?: boolean; // Force re-render on navigation
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### URLPattern Examples
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Static paths
|
|
285
|
+
{ path: '/about' }
|
|
286
|
+
{ path: '/contact' }
|
|
287
|
+
|
|
288
|
+
// Dynamic parameters
|
|
289
|
+
{ path: '/user/:id' }
|
|
290
|
+
{ path: '/blog/:year/:month/:slug' }
|
|
291
|
+
|
|
292
|
+
// Optional parameters
|
|
293
|
+
{ path: '/posts/:id?' }
|
|
294
|
+
|
|
295
|
+
// Wildcard matching
|
|
296
|
+
{ path: '/files/*' }
|
|
297
|
+
{ path: '/api/**' }
|
|
220
298
|
```
|
|
221
299
|
|
|
222
300
|
### Components
|
|
223
301
|
|
|
224
|
-
-
|
|
225
|
-
|
|
302
|
+
#### u-outlet / Outlet
|
|
303
|
+
|
|
304
|
+
Renders the matched route component.
|
|
305
|
+
|
|
306
|
+
```html
|
|
307
|
+
<!-- Lit usage -->
|
|
308
|
+
<u-outlet></u-outlet>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
```jsx
|
|
312
|
+
// React usage
|
|
313
|
+
<Outlet />
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### u-link / Link
|
|
317
|
+
|
|
318
|
+
Smart navigation link with automatic external link detection.
|
|
319
|
+
|
|
320
|
+
```html
|
|
321
|
+
<!-- Lit usage -->
|
|
322
|
+
<u-link href="/about">About</u-link>
|
|
323
|
+
<u-link href="https://example.com">External</u-link>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```jsx
|
|
327
|
+
// React usage
|
|
328
|
+
<Link href="/about">About</Link>
|
|
329
|
+
<Link href="https://example.com">External</Link>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Route Information
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
interface RouteInfo {
|
|
336
|
+
pathname: string; // Current pathname
|
|
337
|
+
search: string; // Query string
|
|
338
|
+
hash: string; // URL hash
|
|
339
|
+
href: string; // Full URL
|
|
340
|
+
basepath: string; // Router base path
|
|
341
|
+
params: Record<string, string>; // Route parameters
|
|
342
|
+
data?: any; // Data from loader function
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Events
|
|
347
|
+
|
|
348
|
+
The router dispatches these events on the `window` object:
|
|
349
|
+
|
|
350
|
+
- **`route-start`** - Navigation begins
|
|
351
|
+
```typescript
|
|
352
|
+
window.addEventListener('route-start', (event) => {
|
|
353
|
+
console.log('Navigating to:', event.detail.pathname);
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
- **`route-end`** - Navigation completes successfully
|
|
358
|
+
```typescript
|
|
359
|
+
window.addEventListener('route-end', (event) => {
|
|
360
|
+
console.log('Navigation complete:', event.detail);
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
- **`route-error`** - Navigation fails
|
|
365
|
+
```typescript
|
|
366
|
+
window.addEventListener('route-error', (event) => {
|
|
367
|
+
console.error('Route error:', event.detail);
|
|
368
|
+
});
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## Advanced Examples
|
|
372
|
+
|
|
373
|
+
### Error Handling
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// Custom error handling
|
|
377
|
+
window.addEventListener('route-error', (event) => {
|
|
378
|
+
const error = event.detail;
|
|
379
|
+
console.error(`Route error (${error.code}):`, error.message);
|
|
380
|
+
|
|
381
|
+
// Show user-friendly error message
|
|
382
|
+
if (error.code === '404') {
|
|
383
|
+
showNotification('Page not found');
|
|
384
|
+
} else {
|
|
385
|
+
showNotification('Navigation failed');
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Dynamic Route Registration
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
const router = new Router({
|
|
394
|
+
root: document.getElementById('app')!,
|
|
395
|
+
routes: [
|
|
396
|
+
{
|
|
397
|
+
path: '/',
|
|
398
|
+
element: 'home-page'
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Add routes dynamically by reconstructing the router
|
|
404
|
+
function addRoute(newRoute: RouteConfig) {
|
|
405
|
+
const newRoutes = [...router.routes, newRoute];
|
|
406
|
+
return new Router({
|
|
407
|
+
root: router.root,
|
|
408
|
+
basepath: router.basepath,
|
|
409
|
+
routes: newRoutes
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
### Progressive Enhancement
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// Check if URLPattern is supported
|
|
418
|
+
if ('URLPattern' in window) {
|
|
419
|
+
// Use the router
|
|
420
|
+
const router = new Router({ /* config */ });
|
|
421
|
+
router.go(window.location.href);
|
|
422
|
+
} else {
|
|
423
|
+
// Fallback to traditional navigation
|
|
424
|
+
console.warn('URLPattern not supported, using traditional navigation');
|
|
425
|
+
}
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Performance Optimization
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
// Lazy load route components
|
|
432
|
+
const routes = [
|
|
433
|
+
{
|
|
434
|
+
path: '/dashboard',
|
|
435
|
+
loader: async () => {
|
|
436
|
+
// Dynamically import the component
|
|
437
|
+
const module = await import('./pages/DashboardPage.js');
|
|
438
|
+
return { component: module.DashboardPage };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Browser Support
|
|
445
|
+
|
|
446
|
+
- **URLPattern API**: Required for routing functionality
|
|
447
|
+
- **Modern browsers**: Chrome 95+, Firefox 106+, Safari 16.4+
|
|
448
|
+
- **Polyfill**: Consider using [urlpattern-polyfill](https://www.npmjs.com/package/urlpattern-polyfill) for older browsers
|
|
226
449
|
|
|
227
|
-
|
|
450
|
+
## Migration Guide
|
|
228
451
|
|
|
229
|
-
|
|
452
|
+
### From v0.1.x to v0.2.x
|
|
230
453
|
|
|
231
|
-
|
|
454
|
+
- Removed `connect()` and `disconnect()` methods
|
|
455
|
+
- Navigation now starts with `router.go()` instead of `router.connect()`
|
|
456
|
+
- Event names changed from `route-change` to `route-start`/`route-end`
|
|
457
|
+
- Simplified component exports - use `UOutlet`/`Outlet` and `ULink`/`Link`
|
|
232
458
|
|
|
233
|
-
|
|
459
|
+
## Contributing
|
|
234
460
|
|
|
235
|
-
|
|
236
|
-
|
|
461
|
+
1. Fork the repository
|
|
462
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
463
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
464
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
465
|
+
5. Open a Pull Request
|
|
237
466
|
|
|
238
467
|
## License
|
|
239
468
|
|
|
240
|
-
MIT
|
|
469
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|