@iyulab/router 0.7.6 → 0.8.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/CHANGELOG.md +10 -0
- package/README.md +79 -213
- package/dist/index.d.ts +40 -7
- package/dist/index.js +23 -12
- package/dist/react.d.ts +11 -3
- package/dist/react.js +1 -1
- package/dist/{share-sbAElOI7.js → share-CUGwxZKa.js} +12 -7
- package/package.json +1 -1
- package/skills/iyulab-router/SKILL.md +28 -6
- package/skills/iyulab-router/references/components.md +43 -0
- package/skills/iyulab-router/references/events-and-errors.md +39 -0
- package/skills/iyulab-router/references/guards-and-metadata.md +50 -0
- package/skills/iyulab-router/references/routing-basics.md +40 -0
- package/skills/iyulab-router/references/url-pattern.md +32 -0
- package/skills/iyulab-router/references/REFERENCE.md +0 -175
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.8.0] - 2026-04-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Added navigation guards via `enter` hooks at both router level (`RouterConfig.enter`) and route level (`RouteConfig.enter`) with redirect/cancel flow support
|
|
7
|
+
- Added `rel` attribute support to `<u-link>` for secure external navigation patterns (for example `noopener noreferrer`)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Breaking:** Renamed route metadata fields from `meta` to `metadata` (`RouteConfig.metadata`, `RouteContext.metadata`)
|
|
11
|
+
- Updated nested outlet resolution to prefer child outlet discovery inside the current outlet, improving deep nested route rendering behavior
|
|
12
|
+
|
|
3
13
|
## [0.7.6] - 2026-04-02
|
|
4
14
|
|
|
5
15
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @iyulab/router
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Client-side SPA router for Lit and React with URLPattern matching, nested routes, and route lifecycle events.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -10,270 +10,136 @@ npm install @iyulab/router
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
-
### Basic Setup
|
|
14
|
-
|
|
15
13
|
```typescript
|
|
16
14
|
import { Router } from '@iyulab/router';
|
|
17
15
|
import { html } from 'lit';
|
|
18
16
|
|
|
19
17
|
const router = new Router({
|
|
18
|
+
root: document.body,
|
|
20
19
|
basepath: '/',
|
|
21
20
|
routes: [
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
render: () => html`<home-page></home-page>`
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
path: '/user/:id', // URLPattern route
|
|
28
|
-
render: (routeInfo) => html`<user-page .userId=${routeInfo.params.id}></user-page>`
|
|
29
|
-
}
|
|
21
|
+
{ index: true, render: () => html`<home-page></home-page>` },
|
|
22
|
+
{ path: '/users/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
|
|
30
23
|
],
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
### Mixed Framework Support
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
import React from 'react';
|
|
38
|
-
|
|
39
|
-
const routes = [
|
|
40
|
-
// Lit component
|
|
41
|
-
{
|
|
42
|
-
path: '/lit-page',
|
|
43
|
-
render: (routeInfo) => {
|
|
44
|
-
return html`<my-lit-component .routeInfo=${routeInfo}></my-lit-component>`
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
// React component
|
|
48
|
-
{
|
|
49
|
-
path: '/react-page',
|
|
50
|
-
render: (routeInfo) => {
|
|
51
|
-
return ( <MyComponent></MyComponent> )
|
|
52
|
-
}
|
|
24
|
+
fallback: {
|
|
25
|
+
render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`,
|
|
53
26
|
},
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
path: '/element-page',
|
|
57
|
-
render: (routeInfo) => {
|
|
58
|
-
const element = document.createElement('my-element');
|
|
59
|
-
element.data = routeInfo.params;
|
|
60
|
-
return element;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
];
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Nested Routes
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
import { RouteConfig } from '@iyulab/router';
|
|
27
|
+
});
|
|
70
28
|
|
|
71
|
-
|
|
72
|
-
{
|
|
73
|
-
path: '/dashboard',
|
|
74
|
-
render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
|
|
75
|
-
children: [
|
|
76
|
-
{
|
|
77
|
-
index: true, // Matches '/dashboard'
|
|
78
|
-
render: () => html`<dashboard-home></dashboard-home>`
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
path: 'settings', // Matches '/dashboard/settings'
|
|
82
|
-
render: () => html`<dashboard-settings></dashboard-settings>`
|
|
83
|
-
}
|
|
84
|
-
]
|
|
85
|
-
}
|
|
86
|
-
];
|
|
29
|
+
router.go('/users/1');
|
|
87
30
|
```
|
|
88
31
|
|
|
89
32
|
## Skills Usage
|
|
90
33
|
|
|
91
|
-
Install the `iyulab-router` skill
|
|
92
|
-
|
|
93
|
-
**Using GitHub shorthand:**
|
|
34
|
+
Install the `iyulab-router` skill for agent-friendly package guidance.
|
|
94
35
|
|
|
95
36
|
```bash
|
|
96
37
|
npx skills add iyulab/node-router
|
|
97
38
|
```
|
|
98
39
|
|
|
99
|
-
**Using local path (after `npm install`):**
|
|
100
|
-
|
|
101
40
|
```bash
|
|
102
41
|
npx skills add ./node_modules/@iyulab/router
|
|
103
42
|
```
|
|
104
43
|
|
|
105
|
-
##
|
|
44
|
+
## Route Guards
|
|
106
45
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
import { LitElement, html } from 'lit';
|
|
111
|
-
import { customElement } from 'lit/decorators.js';
|
|
112
|
-
|
|
113
|
-
import "@iyulab/router";
|
|
114
|
-
|
|
115
|
-
@customElement('app-root')
|
|
116
|
-
export class AppRoot extends LitElement {
|
|
117
|
-
render() {
|
|
118
|
-
return html`
|
|
119
|
-
<nav>
|
|
120
|
-
<u-link href="/">Home</u-link>
|
|
121
|
-
<u-link href="/about">About</u-link>
|
|
122
|
-
<u-link href="/user/123">User Profile</u-link>
|
|
123
|
-
</nav>
|
|
124
|
-
<main>
|
|
125
|
-
<u-outlet></u-outlet>
|
|
126
|
-
</main>
|
|
127
|
-
`;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
### Using with React Components
|
|
133
|
-
|
|
134
|
-
```tsx
|
|
135
|
-
import React from 'react';
|
|
136
|
-
import { UOutlet, ULink } from '@iyulab/router/react';
|
|
137
|
-
|
|
138
|
-
export function AppRoot() {
|
|
139
|
-
return (
|
|
140
|
-
<div>
|
|
141
|
-
<nav>
|
|
142
|
-
<ULink href="/">Home</ULink>
|
|
143
|
-
<ULink href="/about">About</ULink>
|
|
144
|
-
<ULink href="/user/123">User Profile</ULink>
|
|
145
|
-
</nav>
|
|
146
|
-
<main>
|
|
147
|
-
<UOutlet />
|
|
148
|
-
</main>
|
|
149
|
-
</div>
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
## Error Handling
|
|
155
|
-
|
|
156
|
-
The router provides comprehensive error handling through `FallbackRouteContext`. When a routing error occurs, the fallback render function receives a context with full error information:
|
|
46
|
+
Use `enter` to run guard logic before rendering.
|
|
157
47
|
|
|
158
48
|
```typescript
|
|
159
49
|
const router = new Router({
|
|
160
50
|
root: document.body,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return html`<error-page .message=${message}></error-page>`;
|
|
174
|
-
}
|
|
175
|
-
return html`<error-page .error=${ctx.error}></error-page>`;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
51
|
+
enter: (ctx) => {
|
|
52
|
+
if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
53
|
+
return true;
|
|
54
|
+
},
|
|
55
|
+
routes: [
|
|
56
|
+
{ path: '/login', render: () => html`<login-page></login-page>` },
|
|
57
|
+
{
|
|
58
|
+
path: '/admin',
|
|
59
|
+
enter: () => hasRole('admin') || '/forbidden',
|
|
60
|
+
render: () => html`<admin-page></admin-page>`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
178
63
|
});
|
|
179
64
|
```
|
|
180
65
|
|
|
181
|
-
|
|
182
|
-
- `
|
|
183
|
-
- `
|
|
184
|
-
- `
|
|
66
|
+
`enter` return values:
|
|
67
|
+
- `true` (or `undefined`): continue
|
|
68
|
+
- `false`: cancel navigation
|
|
69
|
+
- `string`: redirect to that path
|
|
185
70
|
|
|
186
71
|
## Route Metadata
|
|
187
72
|
|
|
188
|
-
|
|
73
|
+
Attach metadata to routes using `metadata`. The router merges metadata from parent to child and exposes it on `ctx.metadata`.
|
|
189
74
|
|
|
190
75
|
```typescript
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
76
|
+
const routes = [
|
|
77
|
+
{
|
|
78
|
+
path: '/dashboard',
|
|
79
|
+
metadata: { requiresAuth: true, section: 'dashboard' },
|
|
80
|
+
render: () => html`<dashboard-layout><u-outlet></u-outlet></dashboard-layout>`,
|
|
81
|
+
children: [
|
|
82
|
+
{
|
|
83
|
+
path: 'settings',
|
|
84
|
+
metadata: { tab: 'settings' },
|
|
85
|
+
render: (ctx) => html`<settings-page .metadata=${ctx.metadata}></settings-page>`,
|
|
201
86
|
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
meta: { requiresAuth: true, role: 'superadmin' },
|
|
206
|
-
render: (ctx) => {
|
|
207
|
-
// ctx.meta === { requiresAuth: true, layout: 'admin', role: 'superadmin' }
|
|
208
|
-
return html`<admin-settings></admin-settings>`;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
]
|
|
212
|
-
}
|
|
213
|
-
]
|
|
214
|
-
});
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
];
|
|
215
90
|
```
|
|
216
91
|
|
|
217
|
-
|
|
92
|
+
## Nested Routes
|
|
218
93
|
|
|
219
|
-
|
|
94
|
+
Parent routes must render `<u-outlet>` to host child route content.
|
|
220
95
|
|
|
221
|
-
|
|
96
|
+
```typescript
|
|
97
|
+
const routes = [
|
|
98
|
+
{
|
|
99
|
+
path: '/nested',
|
|
100
|
+
render: () => html`<nested-layout><u-outlet></u-outlet></nested-layout>`,
|
|
101
|
+
children: [
|
|
102
|
+
{ index: true, render: () => html`<nested-home></nested-home>` },
|
|
103
|
+
{ path: 'lit', render: () => html`<nested-lit></nested-lit>` },
|
|
104
|
+
{ path: 'react', render: () => <NestedReact /> },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
```
|
|
222
109
|
|
|
223
|
-
|
|
224
|
-
|-------|------|-------------|
|
|
225
|
-
| `route-begin` | `RouteBeginEvent` | Fired when navigation starts |
|
|
226
|
-
| `route-progress` | `RouteProgressEvent` | Fired during async loading (0–100) |
|
|
227
|
-
| `route-done` | `RouteDoneEvent` | Fired when navigation completes successfully |
|
|
228
|
-
| `route-error` | `RouteErrorEvent` | Fired when a routing error occurs |
|
|
110
|
+
## Link and Outlet Components
|
|
229
111
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
window.addEventListener('route-progress', (e: RouteProgressEvent) => {
|
|
233
|
-
progressBar.value = e.progress;
|
|
234
|
-
});
|
|
112
|
+
- `<u-link>`: SPA-aware anchor element
|
|
113
|
+
- `<u-outlet>`: render target for matched route output
|
|
235
114
|
|
|
236
|
-
|
|
237
|
-
window.addEventListener('route-begin', (e: RouteBeginEvent) => {
|
|
238
|
-
console.log('Navigating to:', e.context.pathname);
|
|
239
|
-
});
|
|
115
|
+
`<u-link>` supports `href`, `target`, and `rel`.
|
|
240
116
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
117
|
+
```html
|
|
118
|
+
<u-link href="/docs">Docs</u-link>
|
|
119
|
+
<u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
|
|
120
|
+
```
|
|
244
121
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
122
|
+
React wrappers:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { ULink, UOutlet } from '@iyulab/router/react';
|
|
248
126
|
```
|
|
249
127
|
|
|
250
|
-
##
|
|
128
|
+
## Route Events
|
|
129
|
+
|
|
130
|
+
The router dispatches events on `window`:
|
|
251
131
|
|
|
252
|
-
|
|
132
|
+
- `route-begin`
|
|
133
|
+
- `route-progress`
|
|
134
|
+
- `route-done`
|
|
135
|
+
- `route-error`
|
|
253
136
|
|
|
254
137
|
```typescript
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// Optional parameter
|
|
260
|
-
{ path: '/posts/:category?', render: (ctx) => {
|
|
261
|
-
const category = ctx.params.category || 'all';
|
|
262
|
-
return html`<posts-page .category=${category}></posts-page>`;
|
|
263
|
-
}},
|
|
264
|
-
|
|
265
|
-
// Wildcard (catch-all)
|
|
266
|
-
{ path: '/docs/:path*', render: (ctx) => html`<docs-page .path=${ctx.params.path}></docs-page>` },
|
|
267
|
-
|
|
268
|
-
// Multiple parameters
|
|
269
|
-
{ path: '/org/:orgId/repo/:repoId', render: (ctx) => {
|
|
270
|
-
return html`<repo-page .orgId=${ctx.params.orgId} .repoId=${ctx.params.repoId}></repo-page>`;
|
|
271
|
-
}}
|
|
272
|
-
];
|
|
138
|
+
window.addEventListener('route-progress', (e) => {
|
|
139
|
+
console.log(e.progress);
|
|
140
|
+
});
|
|
273
141
|
```
|
|
274
142
|
|
|
275
|
-
When URL parameters change (e.g., navigating from `/user/1` to `/user/2`), leaf routes (without children) automatically re-render since `force` defaults to `true`. For parent routes with children, set `force: true` explicitly if re-rendering is needed on parameter changes.
|
|
276
|
-
|
|
277
143
|
## License
|
|
278
144
|
|
|
279
|
-
MIT License
|
|
145
|
+
MIT License. See [LICENSE](LICENSE).
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,17 @@ declare interface BaseRouteConfig {
|
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
render?: (ctx: RouteContext) => Promise<unknown> | unknown;
|
|
50
|
+
/**
|
|
51
|
+
* 이 라우트 진입 전에 호출되는 enter 함수입니다.
|
|
52
|
+
* - `string` 반환: 해당 경로로 redirect
|
|
53
|
+
* - `false` 반환: 네비게이션 취소
|
|
54
|
+
* - `true` 반환: 통과
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* { path: '/admin', enter: (ctx) => ctx.metadata.role === 'admin' || '/forbidden' }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
enter?: (ctx: RouteContext) => Promise<string | boolean> | string | boolean;
|
|
50
61
|
/**
|
|
51
62
|
* 라우터 URL 변경시 렌더링을 강제할지 여부
|
|
52
63
|
* - 기본값으로 children을 가질때 false로 설정되며, children이 없을 경우 true로 설정됩니다.
|
|
@@ -63,10 +74,10 @@ declare interface BaseRouteConfig {
|
|
|
63
74
|
* - 인증, SEO, 분석 등의 용도로 사용할 수 있습니다.
|
|
64
75
|
* @example
|
|
65
76
|
* ```typescript
|
|
66
|
-
* { path: '/admin',
|
|
77
|
+
* { path: '/admin', metadata: { requiresAuth: true, role: 'admin' } }
|
|
67
78
|
* ```
|
|
68
79
|
*/
|
|
69
|
-
|
|
80
|
+
metadata?: Record<string, unknown>;
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
/**
|
|
@@ -156,8 +167,6 @@ declare interface RenderOption {
|
|
|
156
167
|
id?: string;
|
|
157
168
|
/** 강제 렌더링 여부 */
|
|
158
169
|
force?: boolean;
|
|
159
|
-
/** 렌더링할 값 */
|
|
160
|
-
value: unknown;
|
|
161
170
|
}
|
|
162
171
|
|
|
163
172
|
/**
|
|
@@ -244,7 +253,7 @@ export declare interface RouteContext {
|
|
|
244
253
|
* 매칭된 라우트 체인의 병합된 메타데이터
|
|
245
254
|
* - 부모 라우트에서 자식 라우트 순서로 병합됩니다.
|
|
246
255
|
*/
|
|
247
|
-
|
|
256
|
+
metadata: Record<string, unknown>;
|
|
248
257
|
}
|
|
249
258
|
|
|
250
259
|
/**
|
|
@@ -316,6 +325,7 @@ export declare class Router {
|
|
|
316
325
|
private readonly _basepath;
|
|
317
326
|
private readonly _routes;
|
|
318
327
|
private readonly _fallback?;
|
|
328
|
+
private readonly _enter?;
|
|
319
329
|
/** 현재 라우팅 요청 ID */
|
|
320
330
|
private _requestID?;
|
|
321
331
|
/** 현재 라우팅 정보 */
|
|
@@ -333,7 +343,7 @@ export declare class Router {
|
|
|
333
343
|
* 지정한 경로의 클라이언트 라우팅을 수행합니다. 상대경로일 경우 basepath와 조합되어 이동합니다.
|
|
334
344
|
* @param href 이동할 경로
|
|
335
345
|
*/
|
|
336
|
-
go(href: string): Promise<
|
|
346
|
+
go(href: string): Promise<undefined>;
|
|
337
347
|
/** 브라우저 히스토리 이벤트가 발생시 라우팅 처리 */
|
|
338
348
|
private handleWindowPopstate;
|
|
339
349
|
/** 클릭 이벤트에서 라우터로 처리할 앵커를 찾아 클라이언트 라우팅 수행 */
|
|
@@ -361,6 +371,19 @@ export declare interface RouterConfig {
|
|
|
361
371
|
* - 라우트는 렌더링할 엘리먼트 또는 컴포넌트를 지정합니다.
|
|
362
372
|
*/
|
|
363
373
|
routes?: RouteConfig[];
|
|
374
|
+
/**
|
|
375
|
+
* 모든 라우트 전환 전에 호출되는 글로벌 enter 함수입니다.
|
|
376
|
+
* - `string` 반환: 해당 경로로 redirect
|
|
377
|
+
* - `false` 반환: 네비게이션 취소
|
|
378
|
+
* - `true` 반환: 통과
|
|
379
|
+
* @example
|
|
380
|
+
* ```typescript
|
|
381
|
+
* enter: async (ctx) => {
|
|
382
|
+
* if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
383
|
+
* }
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
enter?: (ctx: RouteContext) => Promise<string | boolean> | string | boolean;
|
|
364
387
|
/**
|
|
365
388
|
* 라우트 매칭 실패 또는 오류 발생 시 대체 라우트 설정
|
|
366
389
|
* - 지정된 설정이 없을 경우, 기본 오류 페이지가 렌더링됩니다.
|
|
@@ -395,6 +418,16 @@ export declare class ULink extends LitElement {
|
|
|
395
418
|
* - `_top`: 최상위 프레임에서 링크 열기
|
|
396
419
|
*/
|
|
397
420
|
target?: string;
|
|
421
|
+
/**
|
|
422
|
+
* 링크 관계 rel 속성
|
|
423
|
+
*
|
|
424
|
+
* - `noopener`: target이 _blank인 경우 보안 강화 (window.opener 차단)
|
|
425
|
+
* - `noreferrer`: target이 _blank인 경우 보안 강화 + Referer 헤더 제거
|
|
426
|
+
* - `external`: 외부 링크임을 명시 (SEO/접근성에 도움)
|
|
427
|
+
* - `nofollow`: 검색 엔진이 링크를 따라가지 않도록 지시 (SEO에 영향)
|
|
428
|
+
* - 그 외 rel 값도 그대로 전달됩니다.
|
|
429
|
+
*/
|
|
430
|
+
rel?: string;
|
|
398
431
|
/**
|
|
399
432
|
* 링크 대상 URL, 다음 사항에 따라 SPA 라우팅 또는 브라우저 네비게이션이 결정됩니다.
|
|
400
433
|
*
|
|
@@ -436,7 +469,7 @@ export declare class UOutlet extends HTMLElement {
|
|
|
436
469
|
/**
|
|
437
470
|
* 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
|
|
438
471
|
*/
|
|
439
|
-
render(
|
|
472
|
+
render(value: unknown, options?: RenderOption): Promise<void>;
|
|
440
473
|
/**
|
|
441
474
|
* 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
|
|
442
475
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-
|
|
1
|
+
import { a as isExternalUrl, i as absolutePath, n as __decorate, o as parseUrl, r as __decorateMetadata, s as UOutlet, t as ULink } from "./share-CUGwxZKa.js";
|
|
2
2
|
import { LitElement, css, html } from "lit";
|
|
3
3
|
import { customElement, property } from "lit/decorators.js";
|
|
4
4
|
//#region src/types/RouteError.ts
|
|
@@ -214,10 +214,11 @@ function getRandomID() {
|
|
|
214
214
|
* `u-outlet` 엘리먼트를 찾아 반환합니다.
|
|
215
215
|
*
|
|
216
216
|
* @param element 검색을 시작할 HTMLElement
|
|
217
|
+
* @param skip element 자신을 검사에서 제외할지 여부 (기본값: false)
|
|
217
218
|
* @returns 찾은 UOutlet 엘리먼트 또는 undefined
|
|
218
219
|
*/
|
|
219
|
-
function findOutlet(element) {
|
|
220
|
-
if (element
|
|
220
|
+
function findOutlet(element, skip = false) {
|
|
221
|
+
if (!skip && element instanceof UOutlet) return element;
|
|
221
222
|
const roots = element.shadowRoot ? [element.shadowRoot, element] : [element];
|
|
222
223
|
for (const root of roots) for (const child of Array.from(root.children)) {
|
|
223
224
|
const result = findOutlet(child);
|
|
@@ -353,6 +354,7 @@ var Router = class {
|
|
|
353
354
|
this._basepath = absolutePath(config.basepath || "/");
|
|
354
355
|
this._routes = setRoutes(config.routes || [], this._basepath);
|
|
355
356
|
this._fallback = config.fallback;
|
|
357
|
+
this._enter = config.enter;
|
|
356
358
|
window.addEventListener("popstate", this.handleWindowPopstate);
|
|
357
359
|
if (config.useIntercept !== false) document.addEventListener("click", this.handleDocumentClick);
|
|
358
360
|
if (config.initialLoad !== false) waitOutlet(this._rootElement).then(() => {
|
|
@@ -396,14 +398,20 @@ var Router = class {
|
|
|
396
398
|
context.progress = progressCallback;
|
|
397
399
|
let outlet = void 0;
|
|
398
400
|
try {
|
|
401
|
+
if (this._enter) {
|
|
402
|
+
const result = await this._enter(context);
|
|
403
|
+
if (this._requestID !== requestID) return;
|
|
404
|
+
if (typeof result === "string") return void this.go(result);
|
|
405
|
+
if (result === false) return;
|
|
406
|
+
}
|
|
399
407
|
if (this._requestID !== requestID) return;
|
|
400
408
|
window.dispatchEvent(new RouteBeginEvent(context));
|
|
401
409
|
const routes = getRoutes(this._routes, context.pathname);
|
|
402
410
|
const lastRoute = routes[routes.length - 1];
|
|
403
411
|
if (lastRoute && lastRoute.path instanceof URLPattern) context.params = lastRoute.path.exec({ pathname: context.pathname })?.pathname.groups || {};
|
|
404
412
|
const mergedMeta = {};
|
|
405
|
-
for (const route of routes) if (route.
|
|
406
|
-
context.
|
|
413
|
+
for (const route of routes) if (route.metadata) Object.assign(mergedMeta, route.metadata);
|
|
414
|
+
context.metadata = mergedMeta;
|
|
407
415
|
this._context = context;
|
|
408
416
|
outlet = findOutletOrThrow(this._rootElement);
|
|
409
417
|
let title = void 0;
|
|
@@ -411,6 +419,12 @@ var Router = class {
|
|
|
411
419
|
if (routes.length === 0) throw new NotFoundError(context.href);
|
|
412
420
|
for (const route of routes) {
|
|
413
421
|
if (this._requestID !== requestID) return;
|
|
422
|
+
if (route.enter) {
|
|
423
|
+
const result = await route.enter(context);
|
|
424
|
+
if (this._requestID !== requestID) return;
|
|
425
|
+
if (typeof result === "string") return void this.go(result);
|
|
426
|
+
if (result === false) return;
|
|
427
|
+
}
|
|
414
428
|
if (!route.render) continue;
|
|
415
429
|
try {
|
|
416
430
|
content = await route.render(context);
|
|
@@ -419,15 +433,14 @@ var Router = class {
|
|
|
419
433
|
throw new ContentLoadError(LoadError);
|
|
420
434
|
}
|
|
421
435
|
try {
|
|
422
|
-
outlet.render({
|
|
436
|
+
outlet.render(content, {
|
|
423
437
|
id: route.id,
|
|
424
|
-
value: content,
|
|
425
438
|
force: route.force
|
|
426
439
|
});
|
|
427
440
|
} catch (renderError) {
|
|
428
441
|
throw new ContentRenderError(renderError);
|
|
429
442
|
}
|
|
430
|
-
outlet = findOutlet(outlet) || outlet;
|
|
443
|
+
outlet = findOutlet(outlet, true) || outlet;
|
|
431
444
|
title = route.title || title;
|
|
432
445
|
}
|
|
433
446
|
document.title = title || document.title;
|
|
@@ -442,18 +455,16 @@ var Router = class {
|
|
|
442
455
|
...context,
|
|
443
456
|
error: routeError
|
|
444
457
|
});
|
|
445
|
-
outlet.render({
|
|
458
|
+
outlet.render(fallbackContent, {
|
|
446
459
|
id: "#fallback",
|
|
447
|
-
value: fallbackContent,
|
|
448
460
|
force: true
|
|
449
461
|
});
|
|
450
462
|
document.title = this._fallback.title || document.title;
|
|
451
463
|
} else {
|
|
452
464
|
const errorContent = new UErrorPage();
|
|
453
465
|
errorContent.error = error;
|
|
454
|
-
if (outlet) outlet.render({
|
|
466
|
+
if (outlet) outlet.render(errorContent, {
|
|
455
467
|
id: "#error",
|
|
456
|
-
value: errorContent,
|
|
457
468
|
force: true
|
|
458
469
|
});
|
|
459
470
|
else {
|
package/dist/react.d.ts
CHANGED
|
@@ -10,8 +10,6 @@ declare interface RenderOption {
|
|
|
10
10
|
id?: string;
|
|
11
11
|
/** 강제 렌더링 여부 */
|
|
12
12
|
force?: boolean;
|
|
13
|
-
/** 렌더링할 값 */
|
|
14
|
-
value: unknown;
|
|
15
13
|
}
|
|
16
14
|
|
|
17
15
|
/**
|
|
@@ -36,6 +34,16 @@ declare class ULink_2 extends LitElement {
|
|
|
36
34
|
* - `_top`: 최상위 프레임에서 링크 열기
|
|
37
35
|
*/
|
|
38
36
|
target?: string;
|
|
37
|
+
/**
|
|
38
|
+
* 링크 관계 rel 속성
|
|
39
|
+
*
|
|
40
|
+
* - `noopener`: target이 _blank인 경우 보안 강화 (window.opener 차단)
|
|
41
|
+
* - `noreferrer`: target이 _blank인 경우 보안 강화 + Referer 헤더 제거
|
|
42
|
+
* - `external`: 외부 링크임을 명시 (SEO/접근성에 도움)
|
|
43
|
+
* - `nofollow`: 검색 엔진이 링크를 따라가지 않도록 지시 (SEO에 영향)
|
|
44
|
+
* - 그 외 rel 값도 그대로 전달됩니다.
|
|
45
|
+
*/
|
|
46
|
+
rel?: string;
|
|
39
47
|
/**
|
|
40
48
|
* 링크 대상 URL, 다음 사항에 따라 SPA 라우팅 또는 브라우저 네비게이션이 결정됩니다.
|
|
41
49
|
*
|
|
@@ -82,7 +90,7 @@ declare class UOutlet_2 extends HTMLElement {
|
|
|
82
90
|
/**
|
|
83
91
|
* 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
|
|
84
92
|
*/
|
|
85
|
-
render(
|
|
93
|
+
render(value: unknown, options?: RenderOption): Promise<void>;
|
|
86
94
|
/**
|
|
87
95
|
* 기존 DOM을 삭제하여, 초기 상태로 되돌립니다.
|
|
88
96
|
*/
|
package/dist/react.js
CHANGED
|
@@ -9,9 +9,9 @@ var UOutlet = class extends HTMLElement {
|
|
|
9
9
|
/**
|
|
10
10
|
* 주어진 렌더링 옵션에 따라 컨텐츠를 렌더링합니다.
|
|
11
11
|
*/
|
|
12
|
-
async render(
|
|
13
|
-
if (this.routeId === id && force === false) return;
|
|
14
|
-
this.routeId = id;
|
|
12
|
+
async render(value, options) {
|
|
13
|
+
if (this.routeId === options?.id && options?.force === false) return;
|
|
14
|
+
this.routeId = options?.id;
|
|
15
15
|
this.reset();
|
|
16
16
|
if (value === null) throw new Error("Content is null and cannot be rendered.");
|
|
17
17
|
if (typeof value !== "object") throw new Error("Content is not a valid renderable object.");
|
|
@@ -88,7 +88,7 @@ function parseUrl(url, basepath) {
|
|
|
88
88
|
hash: urlObj.hash,
|
|
89
89
|
params: {},
|
|
90
90
|
progress: () => {},
|
|
91
|
-
|
|
91
|
+
metadata: {}
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
/**
|
|
@@ -126,12 +126,12 @@ function catchBasepath(basepath) {
|
|
|
126
126
|
return basepath;
|
|
127
127
|
}
|
|
128
128
|
//#endregion
|
|
129
|
-
//#region \0@oxc-project+runtime@0.
|
|
129
|
+
//#region \0@oxc-project+runtime@0.123.0/helpers/decorateMetadata.js
|
|
130
130
|
function __decorateMetadata(k, v) {
|
|
131
131
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
132
132
|
}
|
|
133
133
|
//#endregion
|
|
134
|
-
//#region \0@oxc-project+runtime@0.
|
|
134
|
+
//#region \0@oxc-project+runtime@0.123.0/helpers/decorate.js
|
|
135
135
|
function __decorate(decorators, target, key, desc) {
|
|
136
136
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
137
137
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -189,7 +189,11 @@ var ULink = class ULink extends LitElement {
|
|
|
189
189
|
}
|
|
190
190
|
render() {
|
|
191
191
|
return html`
|
|
192
|
-
<a
|
|
192
|
+
<a
|
|
193
|
+
href=${this.compute(this.href)}
|
|
194
|
+
target=${ifDefined(this.target)}
|
|
195
|
+
rel=${ifDefined(this.rel)}
|
|
196
|
+
>
|
|
193
197
|
<slot></slot>
|
|
194
198
|
</a>
|
|
195
199
|
`;
|
|
@@ -231,6 +235,7 @@ var ULink = class ULink extends LitElement {
|
|
|
231
235
|
}
|
|
232
236
|
};
|
|
233
237
|
__decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "target", void 0);
|
|
238
|
+
__decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "rel", void 0);
|
|
234
239
|
__decorate([property({ type: String }), __decorateMetadata("design:type", String)], ULink.prototype, "href", void 0);
|
|
235
240
|
ULink = __decorate([customElement("u-link")], ULink);
|
|
236
241
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: iyulab-router
|
|
3
|
-
description: Client-side SPA router for Lit and React with URLPattern
|
|
3
|
+
description: Client-side SPA router for Lit and React with URLPattern matching, nested routes, route guards, metadata merging, fallback handling, and route events. Use when working with @iyulab/router to define routes, add guards, handle navigation, or integrate <u-outlet>/<u-link>.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Browser environments only (requires URLPattern and History API)
|
|
6
6
|
metadata:
|
|
7
7
|
author: iyulab
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.8.0"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# @iyulab/router
|
|
12
12
|
|
|
13
|
-
Client-side router supporting Lit and React renders, nested routes, and URLPattern-based matching.
|
|
13
|
+
Client-side router supporting Lit and React renders, nested routes, route guards, and URLPattern-based matching.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -40,9 +40,19 @@ import { html } from 'lit';
|
|
|
40
40
|
const router = new Router({
|
|
41
41
|
root: document.body, // required — mount element containing <u-outlet>
|
|
42
42
|
basepath: '/', // optional
|
|
43
|
+
enter: (ctx) => {
|
|
44
|
+
if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
45
|
+
return true;
|
|
46
|
+
},
|
|
43
47
|
routes: [
|
|
44
48
|
{ index: true, render: () => html`<home-page></home-page>` },
|
|
45
49
|
{ path: '/user/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
|
|
50
|
+
{
|
|
51
|
+
path: '/admin',
|
|
52
|
+
metadata: { role: 'admin' },
|
|
53
|
+
enter: (ctx) => ctx.metadata.role === 'admin' || '/forbidden',
|
|
54
|
+
render: () => html`<admin-page></admin-page>`,
|
|
55
|
+
},
|
|
46
56
|
],
|
|
47
57
|
fallback: {
|
|
48
58
|
render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`
|
|
@@ -57,9 +67,10 @@ const router = new Router({
|
|
|
57
67
|
| `path` | `string \| URLPattern` | URLPattern path; omit when `index: true` |
|
|
58
68
|
| `index` | `true` | Marks route as index of its parent path |
|
|
59
69
|
| `render` | `(ctx) => unknown` | Returns Lit `TemplateResult`, React element, or `HTMLElement` |
|
|
70
|
+
| `enter` | `(ctx) => string \| boolean \| Promise<string \| boolean>` | Guard before route render (`false` cancel, `string` redirect) |
|
|
60
71
|
| `children` | `RouteConfig[]` | Nested routes; parent must render `<u-outlet>` |
|
|
61
72
|
| `title` | `string` | Sets `document.title` on match |
|
|
62
|
-
| `
|
|
73
|
+
| `metadata` | `Record<string, unknown>` | Arbitrary metadata (auth, layout, analytics) |
|
|
63
74
|
| `force` | `boolean` | Force re-render on URL change (default `true` for leaf routes) |
|
|
64
75
|
|
|
65
76
|
## RouteContext Fields
|
|
@@ -69,7 +80,7 @@ ctx.params // URLPattern captured params
|
|
|
69
80
|
ctx.pathname // path without query/hash
|
|
70
81
|
ctx.path // full path including query + hash
|
|
71
82
|
ctx.query // URLSearchParams
|
|
72
|
-
ctx.
|
|
83
|
+
ctx.metadata // merged metadata from matched route chain
|
|
73
84
|
ctx.progress // (value: number) => void — report 0–100 loading progress
|
|
74
85
|
```
|
|
75
86
|
|
|
@@ -103,6 +114,12 @@ import { ULink } from '@iyulab/router/react';
|
|
|
103
114
|
<ULink href="/about">About</ULink>
|
|
104
115
|
```
|
|
105
116
|
|
|
117
|
+
`<u-link>` supports `href`, `target`, and `rel`.
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
|
|
121
|
+
```
|
|
122
|
+
|
|
106
123
|
## Route Events (window)
|
|
107
124
|
|
|
108
125
|
| Event | Fired when |
|
|
@@ -120,4 +137,9 @@ import { ULink } from '@iyulab/router/react';
|
|
|
120
137
|
| `CONTENT_LOAD_ERROR` | `ContentLoadError` |
|
|
121
138
|
| `CONTENT_RENDER_ERROR` | `ContentRenderError` |
|
|
122
139
|
|
|
123
|
-
|
|
140
|
+
References:
|
|
141
|
+
- [references/routing-basics.md](references/routing-basics.md)
|
|
142
|
+
- [references/url-pattern.md](references/url-pattern.md)
|
|
143
|
+
- [references/guards-and-metadata.md](references/guards-and-metadata.md)
|
|
144
|
+
- [references/components.md](references/components.md)
|
|
145
|
+
- [references/events-and-errors.md](references/events-and-errors.md)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Components
|
|
2
|
+
|
|
3
|
+
## Lit Usage
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import '@iyulab/router';
|
|
7
|
+
import { html } from 'lit';
|
|
8
|
+
|
|
9
|
+
html`
|
|
10
|
+
<nav>
|
|
11
|
+
<u-link href="/">Home</u-link>
|
|
12
|
+
<u-link href="/docs">Docs</u-link>
|
|
13
|
+
<u-link href="https://example.com" target="_blank" rel="noopener noreferrer">External</u-link>
|
|
14
|
+
</nav>
|
|
15
|
+
<main>
|
|
16
|
+
<u-outlet></u-outlet>
|
|
17
|
+
</main>
|
|
18
|
+
`;
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## React Wrappers
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { ULink, UOutlet } from '@iyulab/router/react';
|
|
25
|
+
|
|
26
|
+
export function AppRoot() {
|
|
27
|
+
return (
|
|
28
|
+
<div>
|
|
29
|
+
<nav>
|
|
30
|
+
<ULink href="/">Home</ULink>
|
|
31
|
+
<ULink href="/about">About</ULink>
|
|
32
|
+
</nav>
|
|
33
|
+
<main>
|
|
34
|
+
<UOutlet />
|
|
35
|
+
</main>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Nested Outlet Rule
|
|
42
|
+
|
|
43
|
+
A parent route must render `<u-outlet>` to host child route content.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Events and Errors
|
|
2
|
+
|
|
3
|
+
## Route Events
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
window.addEventListener('route-begin', (e) => {
|
|
7
|
+
console.log(e.context.pathname);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
window.addEventListener('route-progress', (e) => {
|
|
11
|
+
progressBar.value = e.progress;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
window.addEventListener('route-done', (e) => {
|
|
15
|
+
analytics.track(e.context.pathname);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
window.addEventListener('route-error', (e) => {
|
|
19
|
+
errorTracker.report(e.error);
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Fallback
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
fallback: {
|
|
27
|
+
render: (ctx) => {
|
|
28
|
+
const { code, message } = ctx.error;
|
|
29
|
+
if (code === 'NOT_FOUND') return html`<not-found-page></not-found-page>`;
|
|
30
|
+
return html`<error-page .message=${message}></error-page>`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Error Codes
|
|
36
|
+
|
|
37
|
+
- `NOT_FOUND`
|
|
38
|
+
- `CONTENT_LOAD_ERROR`
|
|
39
|
+
- `CONTENT_RENDER_ERROR`
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Guards and Metadata
|
|
2
|
+
|
|
3
|
+
## Global Guard
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const router = new Router({
|
|
7
|
+
root: document.body,
|
|
8
|
+
enter: (ctx) => {
|
|
9
|
+
if (!isAuthenticated() && ctx.pathname !== '/login') return '/login';
|
|
10
|
+
return true;
|
|
11
|
+
},
|
|
12
|
+
routes: [...],
|
|
13
|
+
});
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Route Guard
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
{
|
|
20
|
+
path: '/admin',
|
|
21
|
+
enter: () => hasRole('admin') || '/forbidden',
|
|
22
|
+
render: () => html`<admin-page></admin-page>`
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Guard return values:
|
|
27
|
+
- `true` or `undefined`: continue
|
|
28
|
+
- `false`: cancel navigation
|
|
29
|
+
- `string`: redirect
|
|
30
|
+
|
|
31
|
+
## Route Metadata
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
{
|
|
35
|
+
path: '/admin',
|
|
36
|
+
metadata: { requiresAuth: true, section: 'admin' },
|
|
37
|
+
render: (ctx) => {
|
|
38
|
+
// merged metadata from matched chain
|
|
39
|
+
console.log(ctx.metadata);
|
|
40
|
+
return html`<admin-layout><u-outlet></u-outlet></admin-layout>`;
|
|
41
|
+
},
|
|
42
|
+
children: [
|
|
43
|
+
{
|
|
44
|
+
path: 'settings',
|
|
45
|
+
metadata: { tab: 'settings' },
|
|
46
|
+
render: (ctx) => html`<settings-page .metadata=${ctx.metadata}></settings-page>`
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Routing Basics
|
|
2
|
+
|
|
3
|
+
## Minimal Setup
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Router } from '@iyulab/router';
|
|
7
|
+
import { html } from 'lit';
|
|
8
|
+
|
|
9
|
+
const router = new Router({
|
|
10
|
+
root: document.body,
|
|
11
|
+
basepath: '/',
|
|
12
|
+
routes: [
|
|
13
|
+
{ index: true, render: () => html`<home-page></home-page>` },
|
|
14
|
+
{ path: '/users/:id', render: (ctx) => html`<user-page .id=${ctx.params.id}></user-page>` },
|
|
15
|
+
],
|
|
16
|
+
fallback: {
|
|
17
|
+
render: (ctx) => html`<error-page .error=${ctx.error}></error-page>`,
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## RouterConfig Options
|
|
23
|
+
|
|
24
|
+
| Option | Default | Description |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `root` | - | Mount element (required) |
|
|
27
|
+
| `basepath` | `'/'` | URL base path |
|
|
28
|
+
| `routes` | `[]` | Route definitions |
|
|
29
|
+
| `enter` | - | Global guard before navigation |
|
|
30
|
+
| `fallback` | built-in error page | Error/404 handler |
|
|
31
|
+
| `useIntercept` | `true` | Intercept `<a>` clicks for client routing |
|
|
32
|
+
| `initialLoad` | `true` | Auto-navigate on initialization |
|
|
33
|
+
|
|
34
|
+
## Navigation
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
router.go('/dashboard');
|
|
38
|
+
router.go('settings');
|
|
39
|
+
router.destroy();
|
|
40
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# URL Pattern
|
|
2
|
+
|
|
3
|
+
## Supported Patterns
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
// Required
|
|
7
|
+
{ path: '/user/:id' }
|
|
8
|
+
|
|
9
|
+
// Optional
|
|
10
|
+
{ path: '/posts/:category?' }
|
|
11
|
+
|
|
12
|
+
// Wildcard
|
|
13
|
+
{ path: '/docs/:path*' }
|
|
14
|
+
|
|
15
|
+
// Multiple params
|
|
16
|
+
{ path: '/org/:orgId/repo/:repoId' }
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Access params via `ctx.params`.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
ctx.params.id;
|
|
23
|
+
ctx.params.category;
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Query and Hash
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
ctx.query.get('q');
|
|
30
|
+
ctx.query.get('page');
|
|
31
|
+
ctx.hash;
|
|
32
|
+
```
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# @iyulab/router — Reference
|
|
2
|
-
|
|
3
|
-
## URL Parameter Patterns
|
|
4
|
-
|
|
5
|
-
```ts
|
|
6
|
-
// Required
|
|
7
|
-
{ path: '/user/:id' }
|
|
8
|
-
|
|
9
|
-
// Optional
|
|
10
|
-
{ path: '/posts/:category?' }
|
|
11
|
-
|
|
12
|
-
// Wildcard (catch-all)
|
|
13
|
-
{ path: '/docs/:path*' }
|
|
14
|
-
|
|
15
|
-
// Multiple
|
|
16
|
-
{ path: '/org/:orgId/repo/:repoId' }
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Access via `ctx.params.id`, `ctx.params.category`, etc.
|
|
20
|
-
|
|
21
|
-
## Query String
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
// URL: /search?q=hello&page=2
|
|
25
|
-
ctx.query.get('q') // 'hello'
|
|
26
|
-
ctx.query.get('page') // '2'
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Route Meta
|
|
30
|
-
|
|
31
|
-
Meta from the full matched chain is merged (parent → child, child overrides):
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
{
|
|
35
|
-
path: '/admin',
|
|
36
|
-
meta: { requiresAuth: true, layout: 'admin' },
|
|
37
|
-
render: (ctx) => {
|
|
38
|
-
// ctx.meta === { requiresAuth: true, layout: 'admin' }
|
|
39
|
-
return html`<admin-layout><u-outlet></u-outlet></admin-layout>`;
|
|
40
|
-
},
|
|
41
|
-
children: [
|
|
42
|
-
{
|
|
43
|
-
path: 'settings',
|
|
44
|
-
meta: { role: 'superadmin' },
|
|
45
|
-
render: (ctx) => {
|
|
46
|
-
// ctx.meta === { requiresAuth: true, layout: 'admin', role: 'superadmin' }
|
|
47
|
-
return html`<admin-settings></admin-settings>`;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Async Render with Progress
|
|
55
|
-
|
|
56
|
-
```ts
|
|
57
|
-
{
|
|
58
|
-
path: '/user/:id',
|
|
59
|
-
render: async (ctx) => {
|
|
60
|
-
ctx.progress(20);
|
|
61
|
-
const user = await fetchUser(ctx.params.id);
|
|
62
|
-
ctx.progress(80);
|
|
63
|
-
return html`<user-profile .data=${user}></user-profile>`;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Route Events
|
|
69
|
-
|
|
70
|
-
```ts
|
|
71
|
-
window.addEventListener('route-begin', (e) => {
|
|
72
|
-
console.log('navigating to:', e.context.pathname);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
window.addEventListener('route-progress', (e) => {
|
|
76
|
-
progressBar.value = e.progress; // 0–100
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
window.addEventListener('route-done', (e) => {
|
|
80
|
-
analytics.track(e.context.pathname);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
window.addEventListener('route-error', (e) => {
|
|
84
|
-
errorTracker.report(e.error);
|
|
85
|
-
});
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Error Fallback
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
fallback: {
|
|
92
|
-
render: (ctx) => {
|
|
93
|
-
const { code, message } = ctx.error;
|
|
94
|
-
if (code === 'NOT_FOUND') return html`<not-found-page></not-found-page>`;
|
|
95
|
-
return html`<error-page .message=${message}></error-page>`;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## React Usage
|
|
101
|
-
|
|
102
|
-
```tsx
|
|
103
|
-
import { UOutlet, ULink } from '@iyulab/router/react';
|
|
104
|
-
|
|
105
|
-
export function AppRoot() {
|
|
106
|
-
return (
|
|
107
|
-
<div>
|
|
108
|
-
<nav>
|
|
109
|
-
<ULink href="/">Home</ULink>
|
|
110
|
-
<ULink href="/about">About</ULink>
|
|
111
|
-
</nav>
|
|
112
|
-
<main>
|
|
113
|
-
<UOutlet />
|
|
114
|
-
</main>
|
|
115
|
-
</div>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
Mixed Lit + React routes:
|
|
121
|
-
|
|
122
|
-
```ts
|
|
123
|
-
const routes = [
|
|
124
|
-
{
|
|
125
|
-
path: '/lit-page',
|
|
126
|
-
render: () => html`<my-lit-component></my-lit-component>`
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
path: '/react-page',
|
|
130
|
-
render: () => <MyReactComponent />
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
path: '/raw-element',
|
|
134
|
-
render: (ctx) => {
|
|
135
|
-
const el = document.createElement('my-element');
|
|
136
|
-
el.data = ctx.params;
|
|
137
|
-
return el;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
];
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## RouterConfig Options
|
|
144
|
-
|
|
145
|
-
| Option | Default | Description |
|
|
146
|
-
|---|---|---|
|
|
147
|
-
| `root` | — | Mount element (required) |
|
|
148
|
-
| `basepath` | `'/'` | URL base path |
|
|
149
|
-
| `routes` | `[]` | Route definitions |
|
|
150
|
-
| `fallback` | built-in error page | Error/404 handler |
|
|
151
|
-
| `useIntercept` | `true` | Intercept `<a>` clicks for client routing |
|
|
152
|
-
| `initialLoad` | `true` | Auto-navigate to current URL on init |
|
|
153
|
-
|
|
154
|
-
## Lit Element Integration
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
import "@iyulab/router"; // registers <u-outlet> and <u-link>
|
|
158
|
-
import { LitElement, html } from 'lit';
|
|
159
|
-
import { customElement } from 'lit/decorators.js';
|
|
160
|
-
|
|
161
|
-
@customElement('app-root')
|
|
162
|
-
export class AppRoot extends LitElement {
|
|
163
|
-
render() {
|
|
164
|
-
return html`
|
|
165
|
-
<nav>
|
|
166
|
-
<u-link href="/">Home</u-link>
|
|
167
|
-
<u-link href="/about">About</u-link>
|
|
168
|
-
</nav>
|
|
169
|
-
<main>
|
|
170
|
-
<u-outlet></u-outlet>
|
|
171
|
-
</main>
|
|
172
|
-
`;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
```
|