@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 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 for both Lit and React components
9
- - 📱 Client-side navigation with history management
10
- - 🎯 Nested routing support
11
- - ⚡ Async data loading with loaders
12
- - 🔗 Smart link component with external link detection
13
- - 📊 Route progress tracking
14
- - 🎨 Flexible outlet system for component rendering
15
- - 🌐 Global route information access via `window.route`
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
- ## Usage
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
- // Connect the router
51
- router.connect();
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
- ### Nested Routes
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
- ### Route Information Access
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
- You can access current route information globally via `window.route`:
171
+ Access current route information anywhere in your application:
128
172
 
129
173
  ```typescript
130
- // In any component or script
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('Route data:', window.route.data);
178
+ console.log('Loaded data:', window.route.data);
179
+ ```
180
+
181
+ #### Lit Component Route Integration
135
182
 
136
- // For Lit components
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
- document.addEventListener('route-change', this.onRouteChange);
194
+ window.addEventListener('route-end', this.handleRouteChange);
146
195
  }
147
196
 
148
197
  disconnectedCallback() {
149
- document.removeEventListener('route-change', this.onRouteChange);
198
+ window.removeEventListener('route-end', this.handleRouteChange);
150
199
  super.disconnectedCallback();
151
200
  }
152
201
 
153
- private onRouteChange = () => {
154
- this.requestUpdate(); // Trigger re-render when route changes
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: ${window.route?.pathname}</div>
160
- <div>Params: ${JSON.stringify(window.route?.params)}</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
- // For React components
217
+ #### React Component Route Integration
218
+
219
+ ```tsx
166
220
  import React, { useState, useEffect } from 'react';
167
221
 
168
- export function MyReactComponent() {
222
+ export function RouteAwareComponent() {
169
223
  const [routeInfo, setRouteInfo] = useState(window.route);
170
224
 
171
225
  useEffect(() => {
172
- const handleRouteChange = () => {
173
- setRouteInfo(window.route);
226
+ const handleRouteChange = (event) => {
227
+ setRouteInfo(event.detail);
174
228
  };
175
229
 
176
- document.addEventListener('route-change', handleRouteChange);
177
- return () => document.removeEventListener('route-change', handleRouteChange);
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>Params: {JSON.stringify(routeInfo?.params)}</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
- #### Constructor Options
250
+ ```typescript
251
+ new Router(config: RouterConfig)
252
+ ```
194
253
 
195
- - `root`: HTMLElement - The root element where the router will render components
196
- - `basepath`: string - The base path for the router (optional, defaults to '/')
197
- - `routes`: Route[] - Array of route configurations
198
- - `notfound`: LitElement class or string - Component to render when no route matches
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
- - `connect()`: Connect the router and start listening to navigation events
203
- - `disconnect()`: Disconnect the router and stop listening to events
204
- - `go(href: string)`: Navigate to a specific path
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
- type Route = {
211
- path?: string; // URL pattern (URLPattern syntax)
212
- index?: boolean; // Index route (matches parent path exactly)
213
- element?: typeof LitElement | string; // Lit element to render
214
- component?: ComponentType; // React component to render
215
- loader?: (routeInfo: RouteInfo) => Promise<any>; // Data loader function
216
- title?: string; // Page title
217
- children?: Route[]; // Nested routes
218
- force?: boolean; // Force re-render on navigation
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
- - `UOutlet` / `Outlet`: Outlet component for rendering route components
225
- - `ULink` / `Link`: Smart link component with client-side navigation
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
- ### Global Route Access
450
+ ## Migration Guide
228
451
 
229
- - `window.route`: Global access to current route information (RouteInfo object)
452
+ ### From v0.1.x to v0.2.x
230
453
 
231
- ## Events
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
- The router dispatches the following events:
459
+ ## Contributing
234
460
 
235
- - `route-change`: Fired when the route changes (detail contains RouteInfo)
236
- - `route-progress`: Fired during navigation with progress value (0-1)
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.