@wcstack/router 1.9.1 → 1.10.3

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,513 +1,513 @@
1
- # @wcstack/router
2
-
3
- **What if routing was just HTML tags?**
4
-
5
- Imagine a future where you define your app's navigation structure in markup — nested routes, layouts, typed parameters — all as native HTML elements. No router config objects, no JavaScript ceremony. Just tags that describe where things go.
6
-
7
- That's what `<wcs-router>`, `<wcs-route>`, and friends explore. One CDN import, zero dependencies, pure HTML syntax.
8
-
9
- ## Features
10
-
11
- ### Basic Features
12
- * **Declarative Routing**: Simply list `<wcs-route>` tags within an HTML `<template>`. No JS configuration object required.
13
- * **Nested Route Definitions**: Intuitively express nested structures like `/products/:id`.
14
- * **Parameter Support**: Supports path parameters (`:id`).
15
- * **Fallback (404)**: Handle undefined paths with `<wcs-route fallback>`.
16
- * **Navigation API Based**: Built on the modern standard Navigation API, offering high affinity with native browser behavior.
17
- * **Zero Config / Buildless**: Works directly in the browser without bundling.
18
-
19
- ### Unique Features
20
- * **Light DOM Layout System**: Defines layout templates in normal DOM (Light DOM) without forcing Shadow DOM. Makes global CSS application and `<slot>` insertion easy.
21
- * **Typed Parameters**: Specify type constraints like `:id(int)`. Automatically converts values to `number` type.
22
- * **Mixed Layouts & Routes**: Freely nest `<wcs-layout>` within the routing tree, managing layout switching per area purely through HTML structure.
23
- * **Auto-Binding**: Automatically injects URL parameters into components using `data-bind` attribute (supports `props`, `states`, `attr`, and direct property modes).
24
- * **Declarative `<head>` Management**: Declaratively switch `title` and `meta` tags for each page using `<wcs-head>`.
25
-
26
- ## Usage
27
-
28
- ```html
29
- <wcs-router>
30
- <template>
31
- <!-- When path is "/" -->
32
- <wcs-route path="/">
33
- <!-- Apply the "main-layout" layout -->
34
- <wcs-layout layout="main-layout">
35
- <main-header slot="header"></main-header>
36
- <main-body>
37
- <!-- When path is "/" -->
38
- <wcs-route index>
39
- <wcs-head>
40
- <title>Main Page</title>
41
- </wcs-head>
42
- <main-dashboard></main-dashboard>
43
- </wcs-route>
44
-
45
- <!-- When path is "/products" (relative paths below top-level) -->
46
- <wcs-route path="products">
47
- <wcs-head>
48
- <title>Product Page</title>
49
- </wcs-head>
50
- <!-- When path is "/products" -->
51
- <wcs-route index>
52
- <product-list></product-list>
53
- </wcs-route>
54
- <!-- When path is "/products/:productId" -->
55
- <wcs-route path=":productId">
56
- <!-- productItem.props.productId = productId -->
57
- <product-item data-bind="props"></product-item>
58
- </wcs-route>
59
- </wcs-route>
60
- </main-body>
61
- </wcs-layout>
62
- </wcs-route>
63
-
64
- <!-- When path is "/admin" -->
65
- <wcs-route path="/admin">
66
- <!-- Apply the "admin-layout" layout -->
67
- <wcs-layout layout="admin-layout">
68
- <wcs-head>
69
- <title>Admin Page</title>
70
- </wcs-head>
71
- <admin-header slot="header"></admin-header>
72
- <admin-body></admin-body>
73
- </wcs-layout>
74
- </wcs-route>
75
-
76
- <!-- When no path matches -->
77
- <wcs-route fallback>
78
- <error-404></error-404>
79
- </wcs-route>
80
- </template>
81
- </wcs-router>
82
-
83
- <wcs-outlet>
84
- <!-- Build a DOM tree according to the route path and layout and render it here -->
85
- </wcs-outlet>
86
-
87
- <!-- "main-layout" layout -->
88
- <template id="main-layout">
89
- <section>
90
- <h1> Main </h1>
91
- <slot name="header"></slot>
92
- </section>
93
- <section>
94
- <slot></slot>
95
- </section>
96
- </template>
97
-
98
- <!-- "admin-layout" layout -->
99
- <template id="admin-layout">
100
- <section>
101
- <h1> Admin Main </h1>
102
- <slot name="header"></slot>
103
- </section>
104
- <section>
105
- <slot></slot>
106
- </section>
107
- </template>
108
-
109
- ```
110
-
111
- * <main-header><main-body><main-dashboard><product-list><product-item><admin-header><admin-body><error-404> are custom components in your app.
112
- * The custom elements above must be defined separately (via an autoloader or manual registration).
113
-
114
- ## Reference
115
-
116
- ### Router (wcs-router)
117
-
118
- Define routes and layout slots inside a child template tag. A direct child template tag is required. Outputs according to definitions to `<wcs-outlet>`. Multiple routers can coexist in the same document when each has a distinct `basename`.
119
-
120
- | Attribute | Description |
121
- |------|------|
122
- | `basename` | When routing in a subfolder URL, specify the subfolder. Not required if you don’t run in a subfolder. |
123
-
124
- ### Route (wcs-route)
125
-
126
- Displays children when the route path matches. Match priority is static paths over parameters.
127
-
128
- | Attribute | Description |
129
- |------|------|
130
- | `path` | For top-level routes, specify an absolute path starting with `/`. Otherwise, specify a relative path. For parameters, use `:paramName`. For catch-all, use `*`. Top-level routes cannot use relative paths. |
131
- | `index` | Inherits the upper path. |
132
- | `fallback` | Displayed when no route matches the path. |
133
- | `fullpath` | Path including parent routes (read-only). |
134
- | `name` | Identifier. |
135
- | `guard` | Enables guard handling. Specify the full path to navigate to on guard cancellation. |
136
-
137
- | Property | Description |
138
- |------|------|
139
- | `params` | Matched parameters (strings). |
140
- | `typedParams` | Matched parameters (converted types). |
141
- | `guardHandler` | Sets the guard decision function. |
142
-
143
- Guard decision function type:
144
- `(toPath: string, fromPath: string) => boolean | Promise<boolean>`
145
-
146
- #### GuardHandler (wcs-guard-handler)
147
-
148
- Place as a child of `<wcs-route>` to declaratively define a guard decision function. Export the function as the `default export` from a `<script type="module">`. The `<wcs-guard-handler>` element itself is removed from the DOM after parsing.
149
-
150
- ```html
151
- <wcs-route path="/dashboard" guard="/login">
152
- <wcs-guard-handler>
153
- <script type="module">
154
- export default function(toPath, fromPath) {
155
- return document.cookie.includes('session=');
156
- }
157
- </script>
158
- </wcs-guard-handler>
159
- <dashboard-page></dashboard-page>
160
- </wcs-route>
161
- ```
162
-
163
- - The `guard` attribute value is the redirect path when the guard cancels navigation
164
- - If the function returns `false`, navigation is cancelled and the user is redirected to the `guard` path
165
- - The function can return `Promise<boolean>` for async checks
166
- - `<wcs-guard-handler>` placed outside a `<wcs-route>` is ignored
167
- - If no `<script type="module">` is present, `guardHandler` is not set
168
-
169
- #### Typed Parameters
170
-
171
- By specifying types for path parameters, you can perform value validation and automatic conversion.
172
-
173
- **Syntax**: `:paramName(typeName)`
174
-
175
- ```html
176
- <!-- Integer parameter -->
177
- <wcs-route path="/users/:userId(int)">
178
- <user-detail></user-detail>
179
- </wcs-route>
180
-
181
- <!-- Complex parameters -->
182
- <wcs-route path="/posts/:date(isoDate)/:slug(slug)">
183
- <post-detail></post-detail>
184
- </wcs-route>
185
- ```
186
-
187
- **Built-in Types**:
188
-
189
- | Type Name | Description | Example | Converted Type |
190
- |------|------|------|------|
191
- | `int` | Integer | `123`, `-45` | `number` |
192
- | `float` | Floating point number | `3.14`, `-2.5` | `number` |
193
- | `bool` | Boolean | `true`, `false`, `0`, `1` | `boolean` |
194
- | `uuid` | UUID v1-5 | `550e8400-e29b-41d4-a716-446655440000` | `string` |
195
- | `slug` | Slug (lowercase alphanumeric and hyphens) | `my-post-title` | `string` |
196
- | `isoDate` | ISO 8601 Date | `2024-01-23` | `Date` |
197
- | `any` | Any string (default) | Any | `string` |
198
-
199
- **Retrieving Values**:
200
-
201
- ```javascript
202
- // Get from the route element
203
- const route = document.querySelector('wcs-route[path="/users/:userId(int)"]');
204
-
205
- // Get as string
206
- console.log(route.params.userId); // "123"
207
-
208
- // Get as typed value
209
- console.log(route.typedParams.userId); // 123 (number)
210
- ```
211
-
212
- **Behavior**:
213
- - If the value does not match the type, the route will not match (it does not result in an error).
214
- - If no type is specified, it is treated as `any` (same as previous behavior).
215
- - Specifying an unknown type name also falls back to `any`.
216
-
217
- ### Layout (wcs-layout)
218
-
219
- Loads a template, inserts children into `<slot>`, and writes to `<wcs-layout-outlet>`. Light DOM supported. External file supported.
220
-
221
- | Attribute | Description |
222
- |------|------|
223
- | `layout` | The id attribute of the template tag used as the template. |
224
- | `src` | URL of an external template file. |
225
- | `name` | Identifier passed to `wcs-layout-outlet`. |
226
- | `enable-shadow-root` | Use Shadow DOM in `<wcs-layout-outlet>`. |
227
- | `disable-shadow-root` | Use Light DOM in `<wcs-layout-outlet>`. |
228
-
229
- ### Outlet (wcs-outlet)
230
-
231
- Displays a DOM tree according to the routing and layout settings. Define it in HTML, or if missing it is created by `<wcs-router>`.
232
-
233
- ### LayoutOutlet (wcs-layout-outlet)
234
-
235
- Displays a DOM tree into `<wcs-outlet>` according to the layout (`<wcs-layout>`) settings. Inherits the name attribute from `<wcs-layout>`. Use the name attribute to identify styling targets.
236
-
237
- | Attribute | Description |
238
- |------|------|
239
- | `name` | The name attribute of `<wcs-layout>`. Use it to identify styling targets. |
240
-
241
- #### Light DOM Limitations
242
-
243
- When utilizing `disable-shadow-root` (Light DOM), slot replacement targets **only direct children** of `<wcs-layout>`. Elements with `slot` attributes inside `<wcs-route>` will not be placed in the slot.
244
-
245
- ```html
246
- <!-- NG: <div slot="header"> is not a direct child of wcs-layout, so it doesn't go into the slot -->
247
- <wcs-layout layout="main" disable-shadow-root>
248
- <wcs-route path="/page">
249
- <div slot="header">Header Content</div>
250
- </wcs-route>
251
- </wcs-layout>
252
-
253
- <!-- OK: Make the element with slot attribute a direct child of wcs-layout -->
254
- <wcs-layout layout="main" disable-shadow-root>
255
- <div slot="header">Header Content</div>
256
- <wcs-route path="/page">
257
- <!-- Page content -->
258
- </wcs-route>
259
- </wcs-layout>
260
- ```
261
-
262
- In the case of `enable-shadow-root` (Shadow DOM), this limitation does not apply because the native `<slot>` function is used.
263
-
264
- ### Head (wcs-head)
265
-
266
- Manages document `<head>` elements per route. Uses a stack-based system where the most recently connected Head is prioritized.
267
-
268
- ```html
269
- <wcs-route path="/about">
270
- <wcs-head>
271
- <title>About Us</title>
272
- <meta name="description" content="About our company">
273
- </wcs-head>
274
- <about-page></about-page>
275
- </wcs-route>
276
- ```
277
-
278
- **Supported elements**: `<title>`, `<meta>`, `<link>`, `<base>`, `<script>`, `<style>`
279
-
280
- **Behavior**:
281
- - Captures the initial `<head>` state on first connection
282
- - When multiple `<wcs-head>` elements are active, the last connected one takes priority
283
- - When all `<wcs-head>` elements disconnect, the initial state is restored
284
- - Elements are identified by key (e.g., `<meta>` by `name`/`property`/`http-equiv`, `<link>` by `rel`/`href`)
285
-
286
- ### Link (wcs-link)
287
-
288
- Link. Converted to an `<a>`, and the route path in the `to` attribute is converted to a URL. When the link path matches the current URL, the `active` CSS class is automatically added to the generated `<a>` element.
289
-
290
- | Attribute | Description |
291
- |------|------|
292
- | `to` | Destination path or URL. Paths starting with `/` are treated as internal paths (basename is prepended). Other values are treated as external URLs. |
293
-
294
- **Active state**: The generated `<a>` receives the `active` class when its path matches the current location. Tracking is updated on navigation events (`currententrychange`, `wcs:navigate`, `popstate`).
295
-
296
- ```css
297
- /* Style active links */
298
- a.active { font-weight: bold; color: blue; }
299
- ```
300
-
301
- ## Auto-Binding (`data-bind`)
302
-
303
- Elements with the `data-bind` attribute automatically receive matched route parameters. Four binding modes are available:
304
-
305
- | `data-bind` value | Target | Description |
306
- |------|------|------|
307
- | `"props"` | `element.props` | Merges params into the `props` property |
308
- | `"states"` | `element.states` | Merges params into the `states` property |
309
- | `"attr"` | HTML attributes | Sets params as HTML attributes via `setAttribute()` |
310
- | `""` (empty) | Direct properties | Sets params directly on the element (e.g., `element.id = value`) |
311
-
312
- ```html
313
- <wcs-route path="/users/:userId(int)">
314
- <!-- element.props = { userId: 123 } -->
315
- <user-detail data-bind="props"></user-detail>
316
-
317
- <!-- element.setAttribute("userId", 123) -->
318
- <div data-bind="attr"></div>
319
- </wcs-route>
320
- ```
321
-
322
- Parameters are assigned before `connectedCallback` fires. For custom elements that are not yet defined, assignment is deferred until `customElements.whenDefined()` resolves.
323
-
324
- ## Configuration
325
-
326
- Initialize the router with optional configuration via `bootstrapRouter()`:
327
-
328
- ```javascript
329
- import { bootstrapRouter } from '@wcstack/router';
330
-
331
- bootstrapRouter({
332
- // Custom tag names (all optional)
333
- tagNames: {
334
- router: 'wcs-router', // default
335
- route: 'wcs-route', // default
336
- outlet: 'wcs-outlet', // default
337
- layout: 'wcs-layout', // default
338
- layoutOutlet: 'wcs-layout-outlet', // default
339
- link: 'wcs-link', // default
340
- head: 'wcs-head' // default
341
- },
342
- // Use Shadow DOM for outlets (default: false)
343
- enableShadowRoot: false,
344
- // File extensions stripped from basename (default: [".html"])
345
- basenameFileExtensions: [".html"]
346
- });
347
- ```
348
-
349
- ## Path Specification (Router / Route / Link)
350
-
351
- ### Terminology
352
-
353
- * **URL Pathname**: `location.pathname` (e.g. `/app/products/42`)
354
- * **basename**: The app mount path (e.g. `/app`)
355
- * **internalPath**: The routing path inside the app after removing basename (e.g. `/products/42`)
356
-
357
- ---
358
-
359
- ## 1) basename specification
360
-
361
- ### 1.1 basename resolution order
362
-
363
- 1. The `basename` attribute on `<wcs-router basename="/app">`
364
- 2. If `<base href="/app/">` exists, derive from `new URL(document.baseURI).pathname`
365
- 3. If neither exists, use **empty string** `""` (assumes running at root)
366
-
367
- ### 1.2 basename normalization (important)
368
-
369
- basename is always normalized as follows:
370
-
371
- * Add leading `/` (except empty string)
372
- * Collapse multiple slashes into one
373
- * Remove trailing `/` (except `/` itself, which is treated as empty)
374
- * Treat `.../index.html` or `.../*.html` as files and remove them
375
- * If the result is `/`, basename becomes `""`
376
-
377
- Examples:
378
-
379
- * `"/"` → `""`
380
- * `"/app/"` → `"/app"`
381
- * `"/app/index.html"` → `"/app"`
382
-
383
- ### 1.3 basename and direct links
384
-
385
- * If basename is `""`, no `<base>` exists, and the initial `pathname !== "/"`, it is **an error**
386
- * If basename is `"/app"`:
387
-
388
- * `"/app"` and `"/app/"` are **the same** (app root)
389
- * `"/app"` matches only `"/app"` or `"/app/..."` (does not match `"/appX"`)
390
-
391
- ---
392
-
393
- ## 2) internalPath specification
394
-
395
- ### 2.1 internalPath normalization
396
-
397
- internalPath is always treated as an **absolute path**.
398
-
399
- * Add leading `/`
400
- * Collapse multiple slashes
401
- * Remove trailing `/` (except root `/`)
402
- * If empty, become `/`
403
- * In Router normalization, remove trailing `*.html` when present
404
-
405
- Examples:
406
-
407
- * `""` → `/`
408
- * `"products"` → `/products`
409
- * `"/products/"` → `/products`
410
- * `"///a//b/"` → `/a/b`
411
-
412
- ### 2.2 Get internalPath from URL
413
-
414
- Obtain `internalPath` by matching `URL Pathname` with `basename`.
415
-
416
- * If `pathname === basename`, then `internalPath = "/"`
417
- * If `pathname` starts with `basename + "/"`, then `internalPath = pathname.slice(basename.length)`
418
- * Otherwise `internalPath = pathname`
419
- * If the slice result is `""`, then `internalPath = "/"`
420
-
421
- Examples (basename=`/app`):
422
-
423
- * pathname=`/app` → internalPath=`/`
424
- * pathname=`/app/` → internalPath=`/`
425
- * pathname=`/app/products/42` → internalPath=`/products/42`
426
-
427
- ---
428
-
429
- ## 3) `<wcs-route path="...">` specification
430
-
431
- ### 3.1 path notation
432
-
433
- `path` follows **internalPath rules**.
434
-
435
- * Root (top-level) is `"/"`
436
- * Child routes allow **relative** paths (recommended)
437
-
438
- * Example: parent `/products`, child `":id"` → `/products/:id`
439
-
440
- > In implementation, paths are converted to absolute during parsing.
441
-
442
- ### 3.2 Matching rules
443
-
444
- * **Exact match** by segment
445
- * Parameter `:id` matches a single segment
446
- * Catch-all `*` matches the remaining path (accessible via `params['*']`)
447
-
448
- ### 3.3 Priority (longest match definition)
449
-
450
- If multiple candidates exist, pick the higher priority:
451
-
452
- 1. **More segments**
453
- 2. If same, **more static segments** (`"users"` > `":id"` > `"*"`)
454
- 3. If still same, **definition order**
455
-
456
- > Catch-all `*` has the lowest priority, so more specific routes always take precedence.
457
-
458
- Example:
459
-
460
- * `/admin/users/:id` (static2 + param1)
461
- * `/admin/users/profile` (static3)
462
- → latter wins
463
-
464
- ### 3.4 Trailing slash
465
-
466
- * Matching is done after internal normalization, so
467
-
468
- * `/products` and `/products/` are treated the same (either URL is OK)
469
-
470
- ### 3.5 Catch-all (`*`)
471
-
472
- Specify `*` at the end of a path to match the entire remaining path.
473
-
474
- ```html
475
- <wcs-route path="/admin/profile"></wcs-route> <!-- Priority -->
476
- <wcs-route path="/admin/*"></wcs-route> <!-- Fallback for /admin/xxx -->
477
- <wcs-route path="/*"></wcs-route> <!-- Last resort -->
478
- ```
479
-
480
- | Path | Match | Reason |
481
- |------|-------|--------|
482
- | `/admin/profile` | `/admin/profile` | More segments |
483
- | `/admin/setting` | `/admin/*` | `*` matches `setting` |
484
- | `/admin/a/b/c` | `/admin/*` | `*` matches `a/b/c` |
485
- | `/other` | `/*` | Top-level catch-all |
486
-
487
- The matched remaining path is accessible via `params['*']`.
488
-
489
- ---
490
-
491
- ## 4) `<wcs-link to="...">` specification
492
-
493
- ### 4.1 When `to` starts with `/`
494
-
495
- `to` is treated as **internalPath**.
496
-
497
- * The actual `href` is created by joining `basename + internalPath`
498
- * Join: `"/app" + "/products"` → `"/app/products"` (no `//`)
499
-
500
- ### 4.2 When `to` does not start with `/`
501
-
502
- Treated as an external URL (`new URL(to)` is expected to succeed).
503
-
504
- * Example: `https://example.com/`
505
-
506
- ---
507
-
508
- ## 5) “Drop HTML files” is limited
509
-
510
- Dropping `.html` only applies when the pathname **actually looks like a file**.
511
-
512
- * `"/app/index.html"` → `"/app"` (OK)
513
- * `"/products"` → `"/"` is **NG** (do not drop segments)
1
+ # @wcstack/router
2
+
3
+ **What if routing was just HTML tags?**
4
+
5
+ Imagine a future where you define your app's navigation structure in markup — nested routes, layouts, typed parameters — all as native HTML elements. No router config objects, no JavaScript ceremony. Just tags that describe where things go.
6
+
7
+ That's what `<wcs-router>`, `<wcs-route>`, and friends explore. One CDN import, zero dependencies, pure HTML syntax.
8
+
9
+ ## Features
10
+
11
+ ### Basic Features
12
+ * **Declarative Routing**: Simply list `<wcs-route>` tags within an HTML `<template>`. No JS configuration object required.
13
+ * **Nested Route Definitions**: Intuitively express nested structures like `/products/:id`.
14
+ * **Parameter Support**: Supports path parameters (`:id`).
15
+ * **Fallback (404)**: Handle undefined paths with `<wcs-route fallback>`.
16
+ * **Navigation API Based**: Built on the modern standard Navigation API, offering high affinity with native browser behavior.
17
+ * **Zero Config / Buildless**: Works directly in the browser without bundling.
18
+
19
+ ### Unique Features
20
+ * **Light DOM Layout System**: Defines layout templates in normal DOM (Light DOM) without forcing Shadow DOM. Makes global CSS application and `<slot>` insertion easy.
21
+ * **Typed Parameters**: Specify type constraints like `:id(int)`. Automatically converts values to `number` type.
22
+ * **Mixed Layouts & Routes**: Freely nest `<wcs-layout>` within the routing tree, managing layout switching per area purely through HTML structure.
23
+ * **Auto-Binding**: Automatically injects URL parameters into components using `data-bind` attribute (supports `props`, `states`, `attr`, and direct property modes).
24
+ * **Declarative `<head>` Management**: Declaratively switch `title` and `meta` tags for each page using `<wcs-head>`.
25
+
26
+ ## Usage
27
+
28
+ ```html
29
+ <wcs-router>
30
+ <template>
31
+ <!-- When path is "/" -->
32
+ <wcs-route path="/">
33
+ <!-- Apply the "main-layout" layout -->
34
+ <wcs-layout layout="main-layout">
35
+ <main-header slot="header"></main-header>
36
+ <main-body>
37
+ <!-- When path is "/" -->
38
+ <wcs-route index>
39
+ <wcs-head>
40
+ <title>Main Page</title>
41
+ </wcs-head>
42
+ <main-dashboard></main-dashboard>
43
+ </wcs-route>
44
+
45
+ <!-- When path is "/products" (relative paths below top-level) -->
46
+ <wcs-route path="products">
47
+ <wcs-head>
48
+ <title>Product Page</title>
49
+ </wcs-head>
50
+ <!-- When path is "/products" -->
51
+ <wcs-route index>
52
+ <product-list></product-list>
53
+ </wcs-route>
54
+ <!-- When path is "/products/:productId" -->
55
+ <wcs-route path=":productId">
56
+ <!-- productItem.props.productId = productId -->
57
+ <product-item data-bind="props"></product-item>
58
+ </wcs-route>
59
+ </wcs-route>
60
+ </main-body>
61
+ </wcs-layout>
62
+ </wcs-route>
63
+
64
+ <!-- When path is "/admin" -->
65
+ <wcs-route path="/admin">
66
+ <!-- Apply the "admin-layout" layout -->
67
+ <wcs-layout layout="admin-layout">
68
+ <wcs-head>
69
+ <title>Admin Page</title>
70
+ </wcs-head>
71
+ <admin-header slot="header"></admin-header>
72
+ <admin-body></admin-body>
73
+ </wcs-layout>
74
+ </wcs-route>
75
+
76
+ <!-- When no path matches -->
77
+ <wcs-route fallback>
78
+ <error-404></error-404>
79
+ </wcs-route>
80
+ </template>
81
+ </wcs-router>
82
+
83
+ <wcs-outlet>
84
+ <!-- Build a DOM tree according to the route path and layout and render it here -->
85
+ </wcs-outlet>
86
+
87
+ <!-- "main-layout" layout -->
88
+ <template id="main-layout">
89
+ <section>
90
+ <h1> Main </h1>
91
+ <slot name="header"></slot>
92
+ </section>
93
+ <section>
94
+ <slot></slot>
95
+ </section>
96
+ </template>
97
+
98
+ <!-- "admin-layout" layout -->
99
+ <template id="admin-layout">
100
+ <section>
101
+ <h1> Admin Main </h1>
102
+ <slot name="header"></slot>
103
+ </section>
104
+ <section>
105
+ <slot></slot>
106
+ </section>
107
+ </template>
108
+
109
+ ```
110
+
111
+ * <main-header><main-body><main-dashboard><product-list><product-item><admin-header><admin-body><error-404> are custom components in your app.
112
+ * The custom elements above must be defined separately (via an autoloader or manual registration).
113
+
114
+ ## Reference
115
+
116
+ ### Router (wcs-router)
117
+
118
+ Define routes and layout slots inside a child template tag. A direct child template tag is required. Outputs according to definitions to `<wcs-outlet>`. Multiple routers can coexist in the same document when each has a distinct `basename`.
119
+
120
+ | Attribute | Description |
121
+ |------|------|
122
+ | `basename` | When routing in a subfolder URL, specify the subfolder. Not required if you don’t run in a subfolder. |
123
+
124
+ ### Route (wcs-route)
125
+
126
+ Displays children when the route path matches. Match priority is static paths over parameters.
127
+
128
+ | Attribute | Description |
129
+ |------|------|
130
+ | `path` | For top-level routes, specify an absolute path starting with `/`. Otherwise, specify a relative path. For parameters, use `:paramName`. For catch-all, use `*`. Top-level routes cannot use relative paths. |
131
+ | `index` | Inherits the upper path. |
132
+ | `fallback` | Displayed when no route matches the path. |
133
+ | `fullpath` | Path including parent routes (read-only). |
134
+ | `name` | Identifier. |
135
+ | `guard` | Enables guard handling. Specify the full path to navigate to on guard cancellation. |
136
+
137
+ | Property | Description |
138
+ |------|------|
139
+ | `params` | Matched parameters (strings). |
140
+ | `typedParams` | Matched parameters (converted types). |
141
+ | `guardHandler` | Sets the guard decision function. |
142
+
143
+ Guard decision function type:
144
+ `(toPath: string, fromPath: string) => boolean | Promise<boolean>`
145
+
146
+ #### GuardHandler (wcs-guard-handler)
147
+
148
+ Place as a child of `<wcs-route>` to declaratively define a guard decision function. Export the function as the `default export` from a `<script type="module">`. The `<wcs-guard-handler>` element itself is removed from the DOM after parsing.
149
+
150
+ ```html
151
+ <wcs-route path="/dashboard" guard="/login">
152
+ <wcs-guard-handler>
153
+ <script type="module">
154
+ export default function(toPath, fromPath) {
155
+ return document.cookie.includes('session=');
156
+ }
157
+ </script>
158
+ </wcs-guard-handler>
159
+ <dashboard-page></dashboard-page>
160
+ </wcs-route>
161
+ ```
162
+
163
+ - The `guard` attribute value is the redirect path when the guard cancels navigation
164
+ - If the function returns `false`, navigation is cancelled and the user is redirected to the `guard` path
165
+ - The function can return `Promise<boolean>` for async checks
166
+ - `<wcs-guard-handler>` placed outside a `<wcs-route>` is ignored
167
+ - If no `<script type="module">` is present, `guardHandler` is not set
168
+
169
+ #### Typed Parameters
170
+
171
+ By specifying types for path parameters, you can perform value validation and automatic conversion.
172
+
173
+ **Syntax**: `:paramName(typeName)`
174
+
175
+ ```html
176
+ <!-- Integer parameter -->
177
+ <wcs-route path="/users/:userId(int)">
178
+ <user-detail></user-detail>
179
+ </wcs-route>
180
+
181
+ <!-- Complex parameters -->
182
+ <wcs-route path="/posts/:date(isoDate)/:slug(slug)">
183
+ <post-detail></post-detail>
184
+ </wcs-route>
185
+ ```
186
+
187
+ **Built-in Types**:
188
+
189
+ | Type Name | Description | Example | Converted Type |
190
+ |------|------|------|------|
191
+ | `int` | Integer | `123`, `-45` | `number` |
192
+ | `float` | Floating point number | `3.14`, `-2.5` | `number` |
193
+ | `bool` | Boolean | `true`, `false`, `0`, `1` | `boolean` |
194
+ | `uuid` | UUID v1-5 | `550e8400-e29b-41d4-a716-446655440000` | `string` |
195
+ | `slug` | Slug (lowercase alphanumeric and hyphens) | `my-post-title` | `string` |
196
+ | `isoDate` | ISO 8601 Date | `2024-01-23` | `Date` |
197
+ | `any` | Any string (default) | Any | `string` |
198
+
199
+ **Retrieving Values**:
200
+
201
+ ```javascript
202
+ // Get from the route element
203
+ const route = document.querySelector('wcs-route[path="/users/:userId(int)"]');
204
+
205
+ // Get as string
206
+ console.log(route.params.userId); // "123"
207
+
208
+ // Get as typed value
209
+ console.log(route.typedParams.userId); // 123 (number)
210
+ ```
211
+
212
+ **Behavior**:
213
+ - If the value does not match the type, the route will not match (it does not result in an error).
214
+ - If no type is specified, it is treated as `any` (same as previous behavior).
215
+ - Specifying an unknown type name also falls back to `any`.
216
+
217
+ ### Layout (wcs-layout)
218
+
219
+ Loads a template, inserts children into `<slot>`, and writes to `<wcs-layout-outlet>`. Light DOM supported. External file supported.
220
+
221
+ | Attribute | Description |
222
+ |------|------|
223
+ | `layout` | The id attribute of the template tag used as the template. |
224
+ | `src` | URL of an external template file. |
225
+ | `name` | Identifier passed to `wcs-layout-outlet`. |
226
+ | `enable-shadow-root` | Use Shadow DOM in `<wcs-layout-outlet>`. |
227
+ | `disable-shadow-root` | Use Light DOM in `<wcs-layout-outlet>`. |
228
+
229
+ ### Outlet (wcs-outlet)
230
+
231
+ Displays a DOM tree according to the routing and layout settings. Define it in HTML, or if missing it is created by `<wcs-router>`.
232
+
233
+ ### LayoutOutlet (wcs-layout-outlet)
234
+
235
+ Displays a DOM tree into `<wcs-outlet>` according to the layout (`<wcs-layout>`) settings. Inherits the name attribute from `<wcs-layout>`. Use the name attribute to identify styling targets.
236
+
237
+ | Attribute | Description |
238
+ |------|------|
239
+ | `name` | The name attribute of `<wcs-layout>`. Use it to identify styling targets. |
240
+
241
+ #### Light DOM Limitations
242
+
243
+ When utilizing `disable-shadow-root` (Light DOM), slot replacement targets **only direct children** of `<wcs-layout>`. Elements with `slot` attributes inside `<wcs-route>` will not be placed in the slot.
244
+
245
+ ```html
246
+ <!-- NG: <div slot="header"> is not a direct child of wcs-layout, so it doesn't go into the slot -->
247
+ <wcs-layout layout="main" disable-shadow-root>
248
+ <wcs-route path="/page">
249
+ <div slot="header">Header Content</div>
250
+ </wcs-route>
251
+ </wcs-layout>
252
+
253
+ <!-- OK: Make the element with slot attribute a direct child of wcs-layout -->
254
+ <wcs-layout layout="main" disable-shadow-root>
255
+ <div slot="header">Header Content</div>
256
+ <wcs-route path="/page">
257
+ <!-- Page content -->
258
+ </wcs-route>
259
+ </wcs-layout>
260
+ ```
261
+
262
+ In the case of `enable-shadow-root` (Shadow DOM), this limitation does not apply because the native `<slot>` function is used.
263
+
264
+ ### Head (wcs-head)
265
+
266
+ Manages document `<head>` elements per route. Uses a stack-based system where the most recently connected Head is prioritized.
267
+
268
+ ```html
269
+ <wcs-route path="/about">
270
+ <wcs-head>
271
+ <title>About Us</title>
272
+ <meta name="description" content="About our company">
273
+ </wcs-head>
274
+ <about-page></about-page>
275
+ </wcs-route>
276
+ ```
277
+
278
+ **Supported elements**: `<title>`, `<meta>`, `<link>`, `<base>`, `<script>`, `<style>`
279
+
280
+ **Behavior**:
281
+ - Captures the initial `<head>` state on first connection
282
+ - When multiple `<wcs-head>` elements are active, the last connected one takes priority
283
+ - When all `<wcs-head>` elements disconnect, the initial state is restored
284
+ - Elements are identified by key (e.g., `<meta>` by `name`/`property`/`http-equiv`, `<link>` by `rel`/`href`)
285
+
286
+ ### Link (wcs-link)
287
+
288
+ Link. Converted to an `<a>`, and the route path in the `to` attribute is converted to a URL. When the link path matches the current URL, the `active` CSS class is automatically added to the generated `<a>` element.
289
+
290
+ | Attribute | Description |
291
+ |------|------|
292
+ | `to` | Destination path or URL. Paths starting with `/` are treated as internal paths (basename is prepended). Other values are treated as external URLs. |
293
+
294
+ **Active state**: The generated `<a>` receives the `active` class when its path matches the current location. Tracking is updated on navigation events (`currententrychange`, `wcs:navigate`, `popstate`).
295
+
296
+ ```css
297
+ /* Style active links */
298
+ a.active { font-weight: bold; color: blue; }
299
+ ```
300
+
301
+ ## Auto-Binding (`data-bind`)
302
+
303
+ Elements with the `data-bind` attribute automatically receive matched route parameters. Four binding modes are available:
304
+
305
+ | `data-bind` value | Target | Description |
306
+ |------|------|------|
307
+ | `"props"` | `element.props` | Merges params into the `props` property |
308
+ | `"states"` | `element.states` | Merges params into the `states` property |
309
+ | `"attr"` | HTML attributes | Sets params as HTML attributes via `setAttribute()` |
310
+ | `""` (empty) | Direct properties | Sets params directly on the element (e.g., `element.id = value`) |
311
+
312
+ ```html
313
+ <wcs-route path="/users/:userId(int)">
314
+ <!-- element.props = { userId: 123 } -->
315
+ <user-detail data-bind="props"></user-detail>
316
+
317
+ <!-- element.setAttribute("userId", 123) -->
318
+ <div data-bind="attr"></div>
319
+ </wcs-route>
320
+ ```
321
+
322
+ Parameters are assigned before `connectedCallback` fires. For custom elements that are not yet defined, assignment is deferred until `customElements.whenDefined()` resolves.
323
+
324
+ ## Configuration
325
+
326
+ Initialize the router with optional configuration via `bootstrapRouter()`:
327
+
328
+ ```javascript
329
+ import { bootstrapRouter } from '@wcstack/router';
330
+
331
+ bootstrapRouter({
332
+ // Custom tag names (all optional)
333
+ tagNames: {
334
+ router: 'wcs-router', // default
335
+ route: 'wcs-route', // default
336
+ outlet: 'wcs-outlet', // default
337
+ layout: 'wcs-layout', // default
338
+ layoutOutlet: 'wcs-layout-outlet', // default
339
+ link: 'wcs-link', // default
340
+ head: 'wcs-head' // default
341
+ },
342
+ // Use Shadow DOM for outlets (default: false)
343
+ enableShadowRoot: false,
344
+ // File extensions stripped from basename (default: [".html"])
345
+ basenameFileExtensions: [".html"]
346
+ });
347
+ ```
348
+
349
+ ## Path Specification (Router / Route / Link)
350
+
351
+ ### Terminology
352
+
353
+ * **URL Pathname**: `location.pathname` (e.g. `/app/products/42`)
354
+ * **basename**: The app mount path (e.g. `/app`)
355
+ * **internalPath**: The routing path inside the app after removing basename (e.g. `/products/42`)
356
+
357
+ ---
358
+
359
+ ## 1) basename specification
360
+
361
+ ### 1.1 basename resolution order
362
+
363
+ 1. The `basename` attribute on `<wcs-router basename="/app">`
364
+ 2. If `<base href="/app/">` exists, derive from `new URL(document.baseURI).pathname`
365
+ 3. If neither exists, use **empty string** `""` (assumes running at root)
366
+
367
+ ### 1.2 basename normalization (important)
368
+
369
+ basename is always normalized as follows:
370
+
371
+ * Add leading `/` (except empty string)
372
+ * Collapse multiple slashes into one
373
+ * Remove trailing `/` (except `/` itself, which is treated as empty)
374
+ * Treat `.../index.html` or `.../*.html` as files and remove them
375
+ * If the result is `/`, basename becomes `""`
376
+
377
+ Examples:
378
+
379
+ * `"/"` → `""`
380
+ * `"/app/"` → `"/app"`
381
+ * `"/app/index.html"` → `"/app"`
382
+
383
+ ### 1.3 basename and direct links
384
+
385
+ * If basename is `""`, no `<base>` exists, and the initial `pathname !== "/"`, it is **an error**
386
+ * If basename is `"/app"`:
387
+
388
+ * `"/app"` and `"/app/"` are **the same** (app root)
389
+ * `"/app"` matches only `"/app"` or `"/app/..."` (does not match `"/appX"`)
390
+
391
+ ---
392
+
393
+ ## 2) internalPath specification
394
+
395
+ ### 2.1 internalPath normalization
396
+
397
+ internalPath is always treated as an **absolute path**.
398
+
399
+ * Add leading `/`
400
+ * Collapse multiple slashes
401
+ * Remove trailing `/` (except root `/`)
402
+ * If empty, become `/`
403
+ * In Router normalization, remove trailing `*.html` when present
404
+
405
+ Examples:
406
+
407
+ * `""` → `/`
408
+ * `"products"` → `/products`
409
+ * `"/products/"` → `/products`
410
+ * `"///a//b/"` → `/a/b`
411
+
412
+ ### 2.2 Get internalPath from URL
413
+
414
+ Obtain `internalPath` by matching `URL Pathname` with `basename`.
415
+
416
+ * If `pathname === basename`, then `internalPath = "/"`
417
+ * If `pathname` starts with `basename + "/"`, then `internalPath = pathname.slice(basename.length)`
418
+ * Otherwise `internalPath = pathname`
419
+ * If the slice result is `""`, then `internalPath = "/"`
420
+
421
+ Examples (basename=`/app`):
422
+
423
+ * pathname=`/app` → internalPath=`/`
424
+ * pathname=`/app/` → internalPath=`/`
425
+ * pathname=`/app/products/42` → internalPath=`/products/42`
426
+
427
+ ---
428
+
429
+ ## 3) `<wcs-route path="...">` specification
430
+
431
+ ### 3.1 path notation
432
+
433
+ `path` follows **internalPath rules**.
434
+
435
+ * Root (top-level) is `"/"`
436
+ * Child routes allow **relative** paths (recommended)
437
+
438
+ * Example: parent `/products`, child `":id"` → `/products/:id`
439
+
440
+ > In implementation, paths are converted to absolute during parsing.
441
+
442
+ ### 3.2 Matching rules
443
+
444
+ * **Exact match** by segment
445
+ * Parameter `:id` matches a single segment
446
+ * Catch-all `*` matches the remaining path (accessible via `params['*']`)
447
+
448
+ ### 3.3 Priority (longest match definition)
449
+
450
+ If multiple candidates exist, pick the higher priority:
451
+
452
+ 1. **More segments**
453
+ 2. If same, **more static segments** (`"users"` > `":id"` > `"*"`)
454
+ 3. If still same, **definition order**
455
+
456
+ > Catch-all `*` has the lowest priority, so more specific routes always take precedence.
457
+
458
+ Example:
459
+
460
+ * `/admin/users/:id` (static2 + param1)
461
+ * `/admin/users/profile` (static3)
462
+ → latter wins
463
+
464
+ ### 3.4 Trailing slash
465
+
466
+ * Matching is done after internal normalization, so
467
+
468
+ * `/products` and `/products/` are treated the same (either URL is OK)
469
+
470
+ ### 3.5 Catch-all (`*`)
471
+
472
+ Specify `*` at the end of a path to match the entire remaining path.
473
+
474
+ ```html
475
+ <wcs-route path="/admin/profile"></wcs-route> <!-- Priority -->
476
+ <wcs-route path="/admin/*"></wcs-route> <!-- Fallback for /admin/xxx -->
477
+ <wcs-route path="/*"></wcs-route> <!-- Last resort -->
478
+ ```
479
+
480
+ | Path | Match | Reason |
481
+ |------|-------|--------|
482
+ | `/admin/profile` | `/admin/profile` | More segments |
483
+ | `/admin/setting` | `/admin/*` | `*` matches `setting` |
484
+ | `/admin/a/b/c` | `/admin/*` | `*` matches `a/b/c` |
485
+ | `/other` | `/*` | Top-level catch-all |
486
+
487
+ The matched remaining path is accessible via `params['*']`.
488
+
489
+ ---
490
+
491
+ ## 4) `<wcs-link to="...">` specification
492
+
493
+ ### 4.1 When `to` starts with `/`
494
+
495
+ `to` is treated as **internalPath**.
496
+
497
+ * The actual `href` is created by joining `basename + internalPath`
498
+ * Join: `"/app" + "/products"` → `"/app/products"` (no `//`)
499
+
500
+ ### 4.2 When `to` does not start with `/`
501
+
502
+ Treated as an external URL (`new URL(to)` is expected to succeed).
503
+
504
+ * Example: `https://example.com/`
505
+
506
+ ---
507
+
508
+ ## 5) “Drop HTML files” is limited
509
+
510
+ Dropping `.html` only applies when the pathname **actually looks like a file**.
511
+
512
+ * `"/app/index.html"` → `"/app"` (OK)
513
+ * `"/products"` → `"/"` is **NG** (do not drop segments)