@wavelengthusaf/client-router 0.0.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.
Files changed (169) hide show
  1. package/.gitlab-ci.yml +84 -0
  2. package/.husky/pre-commit +38 -0
  3. package/.storybook/main.ts +16 -0
  4. package/.storybook/manager.ts +7 -0
  5. package/.storybook/preview.ts +23 -0
  6. package/.storybook/theme.ts +30 -0
  7. package/.storybook/vitest.setup.ts +7 -0
  8. package/README.md +556 -0
  9. package/dist/elements/RouteElement.d.ts +0 -0
  10. package/dist/elements/RouteElement.d.ts.map +0 -0
  11. package/dist/elements/RouteElement.js +0 -0
  12. package/dist/elements/RouteElement.js.map +0 -0
  13. package/dist/elements/RouterElement.d.ts +0 -0
  14. package/dist/elements/RouterElement.d.ts.map +0 -0
  15. package/dist/elements/RouterElement.js +0 -0
  16. package/dist/elements/RouterElement.js.map +0 -0
  17. package/dist/elements/__tests__/RouteElement.test.d.ts +0 -0
  18. package/dist/elements/__tests__/RouteElement.test.d.ts.map +0 -0
  19. package/dist/elements/__tests__/RouteElement.test.js +77 -0
  20. package/dist/elements/__tests__/RouteElement.test.js.map +0 -0
  21. package/dist/elements/__tests__/RouterElement.test.d.ts +0 -0
  22. package/dist/elements/__tests__/RouterElement.test.d.ts.map +0 -0
  23. package/dist/elements/__tests__/RouterElement.test.js +0 -0
  24. package/dist/elements/__tests__/RouterElement.test.js.map +0 -0
  25. package/dist/index.d.ts +0 -0
  26. package/dist/index.d.ts.map +0 -0
  27. package/dist/index.js +0 -0
  28. package/dist/index.js.map +0 -0
  29. package/dist/models/CompiledRoute.d.ts +0 -0
  30. package/dist/models/CompiledRoute.d.ts.map +0 -0
  31. package/dist/models/CompiledRoute.js +0 -0
  32. package/dist/models/CompiledRoute.js.map +0 -0
  33. package/dist/models/MatchedRoute.d.ts +0 -0
  34. package/dist/models/MatchedRoute.d.ts.map +0 -0
  35. package/dist/models/MatchedRoute.js +0 -0
  36. package/dist/models/MatchedRoute.js.map +0 -0
  37. package/dist/models/RouteComponent.d.ts +0 -0
  38. package/dist/models/RouteComponent.d.ts.map +0 -0
  39. package/dist/models/RouteComponent.js +0 -0
  40. package/dist/models/RouteComponent.js.map +0 -0
  41. package/dist/models/RouteContext.d.ts +0 -0
  42. package/dist/models/RouteContext.d.ts.map +0 -0
  43. package/dist/models/RouteContext.js +0 -0
  44. package/dist/models/RouteContext.js.map +0 -0
  45. package/dist/models/RouteDefinition.d.ts +0 -0
  46. package/dist/models/RouteDefinition.d.ts.map +0 -0
  47. package/dist/models/RouteDefinition.js +0 -0
  48. package/dist/models/RouteDefinition.js.map +0 -0
  49. package/dist/models/RouteGuard.d.ts +0 -0
  50. package/dist/models/RouteGuard.d.ts.map +0 -0
  51. package/dist/models/RouteGuard.js +0 -0
  52. package/dist/models/RouteGuard.js.map +0 -0
  53. package/dist/models/RouteParams.d.ts +0 -0
  54. package/dist/models/RouteParams.d.ts.map +0 -0
  55. package/dist/models/RouteParams.js +0 -0
  56. package/dist/models/RouteParams.js.map +0 -0
  57. package/dist/models/index.d.ts +0 -0
  58. package/dist/models/index.d.ts.map +0 -0
  59. package/dist/models/index.js +0 -0
  60. package/dist/models/index.js.map +0 -0
  61. package/dist/prettier.config.d.ts +0 -0
  62. package/dist/prettier.config.d.ts.map +0 -0
  63. package/dist/router/Router.d.ts +0 -0
  64. package/dist/router/Router.d.ts.map +0 -0
  65. package/dist/router/Router.js +0 -0
  66. package/dist/router/Router.js.map +0 -0
  67. package/dist/router/__tests__/Router.test.d.ts +0 -0
  68. package/dist/router/__tests__/Router.test.d.ts.map +0 -0
  69. package/dist/router/__tests__/Router.test.js +0 -0
  70. package/dist/router/__tests__/Router.test.js.map +0 -0
  71. package/dist/src/elements/RouteElement.d.ts +0 -0
  72. package/dist/src/elements/RouteElement.d.ts.map +0 -0
  73. package/dist/src/elements/RouterElement.d.ts +0 -0
  74. package/dist/src/elements/RouterElement.d.ts.map +0 -0
  75. package/dist/src/elements/__tests__/RouteElement.test.d.ts +0 -0
  76. package/dist/src/elements/__tests__/RouteElement.test.d.ts.map +0 -0
  77. package/dist/src/elements/__tests__/RouterElement.test.d.ts +0 -0
  78. package/dist/src/elements/__tests__/RouterElement.test.d.ts.map +0 -0
  79. package/dist/src/index.d.ts +0 -0
  80. package/dist/src/index.d.ts.map +0 -0
  81. package/dist/src/models/CompiledRoute.d.ts +0 -0
  82. package/dist/src/models/CompiledRoute.d.ts.map +0 -0
  83. package/dist/src/models/MatchedRoute.d.ts +0 -0
  84. package/dist/src/models/MatchedRoute.d.ts.map +0 -0
  85. package/dist/src/models/RouteComponent.d.ts +0 -0
  86. package/dist/src/models/RouteComponent.d.ts.map +0 -0
  87. package/dist/src/models/RouteContext.d.ts +0 -0
  88. package/dist/src/models/RouteContext.d.ts.map +0 -0
  89. package/dist/src/models/RouteDefinition.d.ts +0 -0
  90. package/dist/src/models/RouteDefinition.d.ts.map +0 -0
  91. package/dist/src/models/RouteGuard.d.ts +0 -0
  92. package/dist/src/models/RouteGuard.d.ts.map +0 -0
  93. package/dist/src/models/RouteParams.d.ts +0 -0
  94. package/dist/src/models/RouteParams.d.ts.map +0 -0
  95. package/dist/src/models/index.d.ts +0 -0
  96. package/dist/src/models/index.d.ts.map +0 -0
  97. package/dist/src/router/Router.d.ts +0 -0
  98. package/dist/src/router/Router.d.ts.map +0 -0
  99. package/dist/src/router/__tests__/Router.test.d.ts +0 -0
  100. package/dist/src/router/__tests__/Router.test.d.ts.map +0 -0
  101. package/dist/src/stories/RouterElement.stories.d.ts +0 -0
  102. package/dist/src/stories/RouterElement.stories.d.ts.map +0 -0
  103. package/dist/src/stories/RouterElement.stories.test.d.ts +0 -0
  104. package/dist/src/stories/RouterElement.stories.test.d.ts.map +0 -0
  105. package/dist/src/utils/__tests__/compileRoute.test.d.ts +0 -0
  106. package/dist/src/utils/__tests__/compileRoute.test.d.ts.map +0 -0
  107. package/dist/src/utils/__tests__/matchRoute.test.d.ts +0 -0
  108. package/dist/src/utils/__tests__/matchRoute.test.d.ts.map +0 -0
  109. package/dist/src/utils/compileRoute.d.ts +0 -0
  110. package/dist/src/utils/compileRoute.d.ts.map +0 -0
  111. package/dist/src/utils/index.d.ts +0 -0
  112. package/dist/src/utils/index.d.ts.map +0 -0
  113. package/dist/src/utils/matchRoute.d.ts +0 -0
  114. package/dist/src/utils/matchRoute.d.ts.map +0 -0
  115. package/dist/stories/RouterElement.stories.d.ts +0 -0
  116. package/dist/stories/RouterElement.stories.d.ts.map +0 -0
  117. package/dist/stories/RouterElement.stories.js +0 -0
  118. package/dist/stories/RouterElement.stories.js.map +0 -0
  119. package/dist/utils/__tests__/compileRoute.test.d.ts +0 -0
  120. package/dist/utils/__tests__/compileRoute.test.d.ts.map +0 -0
  121. package/dist/utils/__tests__/compileRoute.test.js +0 -0
  122. package/dist/utils/__tests__/compileRoute.test.js.map +0 -0
  123. package/dist/utils/__tests__/matchRoute.test.d.ts +0 -0
  124. package/dist/utils/__tests__/matchRoute.test.d.ts.map +0 -0
  125. package/dist/utils/__tests__/matchRoute.test.js +0 -0
  126. package/dist/utils/__tests__/matchRoute.test.js.map +0 -0
  127. package/dist/utils/compileRoute.d.ts +0 -0
  128. package/dist/utils/compileRoute.d.ts.map +0 -0
  129. package/dist/utils/compileRoute.js +0 -0
  130. package/dist/utils/compileRoute.js.map +0 -0
  131. package/dist/utils/index.d.ts +0 -0
  132. package/dist/utils/index.d.ts.map +0 -0
  133. package/dist/utils/index.js +0 -0
  134. package/dist/utils/index.js.map +0 -0
  135. package/dist/utils/matchRoute.d.ts +0 -0
  136. package/dist/utils/matchRoute.d.ts.map +0 -0
  137. package/dist/utils/matchRoute.js +0 -0
  138. package/dist/utils/matchRoute.js.map +0 -0
  139. package/eslint.config.mjs +89 -0
  140. package/index.ts +15 -0
  141. package/package.json +54 -0
  142. package/prettier.config.ts +30 -0
  143. package/src/elements/RouteElement.ts +67 -0
  144. package/src/elements/RouterElement.ts +97 -0
  145. package/src/elements/__tests__/RouteElement.test.ts +89 -0
  146. package/src/elements/__tests__/RouterElement.test.ts +253 -0
  147. package/src/index.ts +15 -0
  148. package/src/models/CompiledRoute.ts +7 -0
  149. package/src/models/MatchedRoute.ts +7 -0
  150. package/src/models/RouteComponent.ts +6 -0
  151. package/src/models/RouteContext.ts +7 -0
  152. package/src/models/RouteDefinition.ts +6 -0
  153. package/src/models/RouteGuard.ts +6 -0
  154. package/src/models/RouteParams.ts +3 -0
  155. package/src/models/index.ts +17 -0
  156. package/src/router/Router.ts +225 -0
  157. package/src/router/__tests__/Router.test.ts +464 -0
  158. package/src/stories/RouterElement.stories.test.ts +310 -0
  159. package/src/stories/RouterElement.stories.ts +208 -0
  160. package/src/utils/__tests__/compileRoute.test.ts +97 -0
  161. package/src/utils/__tests__/matchRoute.test.ts +180 -0
  162. package/src/utils/compileRoute.ts +37 -0
  163. package/src/utils/index.ts +4 -0
  164. package/src/utils/matchRoute.ts +37 -0
  165. package/tsconfig.build.json +20 -0
  166. package/tsconfig.json +55 -0
  167. package/vitest.config.ts +34 -0
  168. package/vitest.setup.ts +3 -0
  169. package/vitest.shims.d.ts +1 -0
package/README.md ADDED
@@ -0,0 +1,556 @@
1
+ # Wavelength Router
2
+
3
+ A lightweight, native TypeScript web components router for modern web applications.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Getting Started](#getting-started)
8
+ - [Prerequisites](#prerequisites)
9
+ - [Installation](#installation)
10
+ - [Husky Setup](#husky-setup)
11
+ - [Adding to Your Project](#adding-to-your-project)
12
+ - [Usage](#usage)
13
+ - [HTML Template Approach](#html-template-approach)
14
+ - [TypeScript Approach](#typescript-approach)
15
+ - [API Reference](#api-reference)
16
+ - [`<wavelength-router>` Attributes](#wavelength-router-attributes)
17
+ - [`<wavelength-route>` Attributes](#wavelength-route-attributes)
18
+ - [RouteContext](#routecontext)
19
+ - [RouteGuard](#routeguard)
20
+ - [Programmatic Navigation](#programmatic-navigation)
21
+ - [Best Practices](#best-practices)
22
+
23
+ ---
24
+
25
+ ## Getting Started
26
+
27
+ ### Prerequisites
28
+
29
+ - Node.js 18+ and npm
30
+ - A modern browser with native web components support
31
+
32
+ ### Installation
33
+
34
+ Clone the repository and install dependencies:
35
+
36
+ ```bash
37
+ git clone https://github.com/your-org/wavelength-router.git
38
+ cd wavelength-router
39
+ npm install
40
+ ```
41
+
42
+ ### Husky Setup
43
+
44
+ This project uses Husky to run linting and tests before each commit. If you're setting up the project for the first time or if Husky hooks aren't working, initialize it with:
45
+
46
+ ```bash
47
+ npm run prepare
48
+ ```
49
+
50
+ Or manually initialize:
51
+
52
+ ```bash
53
+ npx husky init
54
+ ```
55
+
56
+ This ensures that before every commit, Husky will:
57
+
58
+ 1. Run unit tests via `npm run test`
59
+ 2. Run ESLint and Prettier via `npm run lint`
60
+
61
+ If either fails, the commit will be blocked.
62
+
63
+ ---
64
+
65
+ ## Adding to Your Project
66
+
67
+ ### Option 1: npm Package (Published)
68
+
69
+ Once published to npm, install the package:
70
+
71
+ ```bash
72
+ npm install @wavelengthusaf/client-router
73
+ ```
74
+
75
+ Then import the elements in your entry point:
76
+
77
+ ```typescript
78
+ // main.ts or main.js
79
+ import '@wavelengthusaf/client-router';
80
+ ```
81
+
82
+ ### Option 2: Local Development
83
+
84
+ For local development or testing changes:
85
+
86
+ ```bash
87
+ npm run build
88
+ ```
89
+
90
+ This compiles TypeScript to `dist/`. Then in your consuming project:
91
+
92
+ ```typescript
93
+ import '@wavelengthusaf/client-router';
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Usage
99
+
100
+ You can implement routing using either the HTML template approach or pure TypeScript.
101
+
102
+ ### HTML Template Approach
103
+
104
+ Use the custom elements directly in your HTML. This is ideal for simpler applications or when you want declarative routing.
105
+
106
+ #### Basic Example
107
+
108
+ ```html
109
+ <!DOCTYPE html>
110
+ <html lang="en">
111
+ <head>
112
+ <meta charset="UTF-8" />
113
+ <title>My App</title>
114
+ </head>
115
+ <body>
116
+ <!-- Navigation -->
117
+ <nav>
118
+ <a href="/">Home</a>
119
+ <a href="/about">About</a>
120
+ <a href="/users/123">User Profile</a>
121
+ </nav>
122
+
123
+ <!-- Router Container - renders matched route component here -->
124
+ <wavelength-router>
125
+ <wavelength-route path="/" component="home-page"></wavelength-route>
126
+ <wavelength-route path="/about" component="about-page"></wavelength-route>
127
+ <wavelength-route
128
+ path="/users/:userId"
129
+ component="user-page"
130
+ ></wavelength-route>
131
+ <wavelength-route path="*" component="not-found-page"></wavelength-route>
132
+ </wavelength-router>
133
+
134
+ <script type="module">
135
+ import '@wavelengthusaf/client-router';
136
+
137
+ // Define your page components as custom elements
138
+ customElements.define(
139
+ 'home-page',
140
+ class extends HTMLElement {
141
+ connectedCallback() {
142
+ this.innerHTML = '<h1>Welcome Home</h1>';
143
+ }
144
+ },
145
+ );
146
+
147
+ customElements.define(
148
+ 'about-page',
149
+ class extends HTMLElement {
150
+ connectedCallback() {
151
+ this.innerHTML = '<h1>About Us</h1><p>We are awesome.</p>';
152
+ }
153
+ },
154
+ );
155
+
156
+ customElements.define(
157
+ 'user-page',
158
+ class extends HTMLElement {
159
+ connectedCallback() {
160
+ // Access route parameters via routeContext
161
+ const userId = this.routeContext?.routeParams?.userId;
162
+ this.innerHTML = `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
163
+ }
164
+ },
165
+ );
166
+
167
+ customElements.define(
168
+ 'not-found-page',
169
+ class extends HTMLElement {
170
+ connectedCallback() {
171
+ this.innerHTML = '<h1>404 - Page Not Found</h1>';
172
+ }
173
+ },
174
+ );
175
+ </script>
176
+ </body>
177
+ </html>
178
+ ```
179
+
180
+ #### How It Works
181
+
182
+ 1. `<wavelength-router>` acts as the router container and outlet
183
+ 2. It scans for `<wavelength-route>` children and extracts route definitions
184
+ 3. When the URL changes, the router matches the path and mounts the corresponding component
185
+ 4. The component is rendered inside the `<wavelength-router>` element
186
+
187
+ ---
188
+
189
+ ### TypeScript Approach
190
+
191
+ For more control, you can use the `Router` class directly and define routes programmatically.
192
+
193
+ #### Basic Example
194
+
195
+ ```typescript
196
+ import { Router } from '@wavelengthusaf/client-router';
197
+ import type {
198
+ RouteContext,
199
+ RouteDefinition,
200
+ RouteGuard,
201
+ } from '@wavelengthusaf/client-router';
202
+
203
+ // Define your page components
204
+ class HomePage extends HTMLElement {
205
+ connectedCallback() {
206
+ this.innerHTML = '<h1>Welcome Home</h1>';
207
+ }
208
+ }
209
+
210
+ class AboutPage extends HTMLElement {
211
+ connectedCallback() {
212
+ this.innerHTML = '<h1>About Us</h1>';
213
+ }
214
+ }
215
+
216
+ class UserPage extends HTMLElement {
217
+ connectedCallback() {
218
+ const userId = this.routeContext?.routeParams?.userId;
219
+ this.innerHTML = `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
220
+ }
221
+ }
222
+
223
+ // Register components
224
+ customElements.define('home-page', HomePage);
225
+ customElements.define('about-page', AboutPage);
226
+ customElements.define('user-page', UserPage);
227
+
228
+ // Define routes using RouteDefinition objects
229
+ const routes: RouteDefinition[] = [
230
+ {
231
+ path: '/',
232
+ routeComponent: {
233
+ mount(container: HTMLElement, context: RouteContext) {
234
+ const element = document.createElement('home-page') as HTMLElement & {
235
+ routeContext?: RouteContext;
236
+ };
237
+ element.routeContext = context;
238
+ container.appendChild(element);
239
+ },
240
+ unmount() {
241
+ // Optional cleanup
242
+ },
243
+ },
244
+ },
245
+ {
246
+ path: '/about',
247
+ routeComponent: {
248
+ mount(container: HTMLElement, context: RouteContext) {
249
+ const element = document.createElement('about-page') as HTMLElement & {
250
+ routeContext?: RouteContext;
251
+ };
252
+ element.routeContext = context;
253
+ container.appendChild(element);
254
+ },
255
+ },
256
+ },
257
+ {
258
+ path: '/users/:userId',
259
+ routeComponent: {
260
+ mount(container: HTMLElement, context: RouteContext) {
261
+ const element = document.createElement('user-page') as HTMLElement & {
262
+ routeContext?: RouteContext;
263
+ };
264
+ element.routeContext = context;
265
+ container.appendChild(element);
266
+ },
267
+ },
268
+ },
269
+ ];
270
+
271
+ // Create and start the router
272
+ const router = new Router(routes, {
273
+ outlet: document.getElementById('app')!, // Container element
274
+ hashMode: false, // Use hash routing (optional)
275
+ });
276
+
277
+ router.start();
278
+ ```
279
+
280
+ #### RouteDefinition Structure
281
+
282
+ Each route requires:
283
+
284
+ - `path`: The URL path pattern (supports `:param` syntax for dynamic segments)
285
+ - `routeComponent`: An object with a `mount` function (and optional `unmount`)
286
+
287
+ ```typescript
288
+ interface RouteDefinition {
289
+ path: string; // e.g., '/users/:id' or '/products/:category/:id'
290
+ routeComponent: {
291
+ mount(container: HTMLElement, context: RouteContext): void;
292
+ unmount?(): void; // Optional cleanup function
293
+ };
294
+ }
295
+ ```
296
+
297
+ ---
298
+
299
+ ## API Reference
300
+
301
+ ### `<wavelength-router>` Attributes
302
+
303
+ | Attribute | Type | Description |
304
+ | ----------- | ------ | ------------------------------------------------------- |
305
+ | `hash-mode` | flag | Enable hash-based routing (`#/path`) instead of history |
306
+ | `guard` | string | Name of a global guard function on `window` |
307
+
308
+ #### Example with Hash Mode
309
+
310
+ ```html
311
+ <wavelength-router hash-mode>
312
+ <wavelength-route path="/" component="home-page"></wavelength-route>
313
+ <wavelength-route path="/profile" component="profile-page"></wavelength-route>
314
+ </wavelength-router>
315
+ ```
316
+
317
+ URL becomes: `https://example.com/#/profile`
318
+
319
+ ### `<wavelength-route>` Attributes
320
+
321
+ | Attribute | Type | Description |
322
+ | ----------- | ------ | ------------------------------------------------ |
323
+ | `path` | string | URL path pattern (supports `:param` syntax) |
324
+ | `component` | string | Custom element tag name to render for this route |
325
+
326
+ #### Path Patterns
327
+
328
+ - Static: `/about`, `/dashboard`
329
+ - Dynamic: `/users/:userId`, `/posts/:category/:postId`
330
+ - Wildcard: `*` (catches all unmatched routes)
331
+
332
+ ### RouteContext
333
+
334
+ Passed to your component's `routeContext` property and the mount function:
335
+
336
+ ```typescript
337
+ interface RouteContext {
338
+ path: string; // The matched path, e.g., '/users/123'
339
+ routeParams: {
340
+ // Extracted URL parameters
341
+ [key: string]: string;
342
+ };
343
+ query: URLSearchParams; // Query string parameters
344
+ }
345
+ ```
346
+
347
+ #### Accessing RouteContext in Components
348
+
349
+ ```typescript
350
+ class MyPage extends HTMLElement {
351
+ static get routeContext(): RouteContext | undefined {
352
+ return (this as unknown as { routeContext?: RouteContext }).routeContext;
353
+ }
354
+
355
+ connectedCallback() {
356
+ const ctx = MyPage.routeContext;
357
+ if (ctx) {
358
+ console.log('Current path:', ctx.path);
359
+ console.log('User ID:', ctx.routeParams.userId);
360
+ console.log('Search query:', ctx.query.get('search'));
361
+ }
362
+ }
363
+ }
364
+ ```
365
+
366
+ ### RouteGuard
367
+
368
+ Navigation guards allow you to intercept and redirect navigation. Define a guard function and reference it via the `guard` attribute.
369
+
370
+ ```typescript
371
+ // Define guard on window (required for HTML approach)
372
+ type NextCallback = (redirect?: string) => void;
373
+
374
+ (window as unknown as Record<string, unknown>).authGuard = (
375
+ to: RouteContext,
376
+ next: NextCallback,
377
+ ) => {
378
+ const isAuthenticated = checkAuth(); // Your auth logic
379
+
380
+ if (to.path.startsWith('/protected') && !isAuthenticated) {
381
+ next('/login'); // Redirect to login
382
+ } else {
383
+ next(); // Allow navigation
384
+ }
385
+ };
386
+ ```
387
+
388
+ ```html
389
+ <wavelength-router guard="authGuard">
390
+ <wavelength-route path="/" component="home-page"></wavelength-route>
391
+ <wavelength-route
392
+ path="/protected"
393
+ component="protected-page"
394
+ ></wavelength-route>
395
+ <wavelength-route path="/login" component="login-page"></wavelength-route>
396
+ </wavelength-router>
397
+ ```
398
+
399
+ **Guard Flow:**
400
+
401
+ 1. Guard receives `RouteContext` with destination path
402
+ 2. Guard calls `next()` to allow navigation or `next('/redirect-path')` to redirect
403
+
404
+ ### Programmatic Navigation
405
+
406
+ The router provides navigation methods:
407
+
408
+ ```typescript
409
+ const routerElement = document.querySelector('wavelength-router');
410
+
411
+ // Navigate to a new path (adds to history)
412
+ routerElement.navigate('/about');
413
+
414
+ // Navigate and replace current entry (no history)
415
+ routerElement.replace('/new-page');
416
+
417
+ // Go back in history
418
+ routerElement.back();
419
+ ```
420
+
421
+ ---
422
+
423
+ ## Best Practices
424
+
425
+ ### 1. Always Define a Wildcard Route
426
+
427
+ Provide a catch-all route for 404 handling:
428
+
429
+ ```html
430
+ <wavelength-route path="*" component="not-found-page"></wavelength-route>
431
+ ```
432
+
433
+ ### 2. Access RouteContext Correctly
434
+
435
+ The `routeContext` is injected into your custom element. Always check for its existence:
436
+
437
+ ```typescript
438
+ class UserPage extends HTMLElement {
439
+ connectedCallback() {
440
+ const ctx = (this as unknown as { routeContext?: RouteContext })
441
+ .routeContext;
442
+ if (ctx) {
443
+ this.innerHTML = `<p>User: ${ctx.routeParams.userId}</p>`;
444
+ }
445
+ }
446
+ }
447
+ ```
448
+
449
+ ### 3. Use Hash Mode for Static Hosts
450
+
451
+ If deploying to static hosting without server configuration, use hash mode:
452
+
453
+ ```html
454
+ <wavelength-router hash-mode> ... </wavelength-router>
455
+ ```
456
+
457
+ This avoids the need for server-side routing rules.
458
+
459
+ ### 4. Clean Up in Route Components
460
+
461
+ Implement cleanup logic in the `unmount` function if needed:
462
+
463
+ ```typescript
464
+ const routes: RouteDefinition[] = [
465
+ {
466
+ path: '/chat',
467
+ routeComponent: {
468
+ mount(container, context) {
469
+ const chat = document.createElement('chat-widget');
470
+ container.appendChild(chat);
471
+ },
472
+ unmount() {
473
+ // Close WebSocket connections, remove event listeners, etc.
474
+ },
475
+ },
476
+ },
477
+ ];
478
+ ```
479
+
480
+ ### 5. Route Guards for Authentication
481
+
482
+ Always protect authenticated routes:
483
+
484
+ ```typescript
485
+ (window as unknown as Record<string, unknown>).requireAuth = (ctx, next) => {
486
+ if (isProtectedPath(ctx.path) && !getCurrentUser()) {
487
+ next('/login');
488
+ } else {
489
+ next();
490
+ }
491
+ };
492
+ ```
493
+
494
+ ### 6. URL Encoding
495
+
496
+ Route parameters are automatically URL-decoded. If you pass special characters, they will be properly handled.
497
+
498
+ ### 7. Order Routes by Specificity
499
+
500
+ More specific routes should be defined before general ones:
501
+
502
+ ```html
503
+ <!-- Correct order -->
504
+ <wavelength-route path="/users/:id" component="user-page"></wavelength-route>
505
+ <wavelength-route path="/users" component="users-list"></wavelength-route>
506
+ <wavelength-route path="*" component="not-found"></wavelength-route>
507
+ ```
508
+
509
+ ### 8. Avoid Memory Leaks
510
+
511
+ Always clean up when routes unmount:
512
+
513
+ ```typescript
514
+ routeComponent: {
515
+ let interval: number;
516
+
517
+ mount(container) {
518
+ const page = document.createElement('my-page');
519
+ container.appendChild(page);
520
+
521
+ // Store reference for cleanup
522
+ interval = setInterval(() => updateTime(), 1000);
523
+ },
524
+ unmount() {
525
+ clearInterval(interval);
526
+ },
527
+ }
528
+ ```
529
+
530
+ ---
531
+
532
+ ## Development
533
+
534
+ ```bash
535
+ # Run tests
536
+ npm run test
537
+
538
+ # Run tests with coverage
539
+ npm run test -- --coverage
540
+
541
+ # Lint and format
542
+ npm run lint
543
+ npm run prettier
544
+
545
+ # Build for production
546
+ npm run build
547
+
548
+ # Run Storybook for component development
549
+ npm run storybook
550
+ ```
551
+
552
+ ---
553
+
554
+ ## License
555
+
556
+ ISC - Wavelength
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ import { RouteElement } from '../RouteElement.ts';
3
+ /* eslint-disable @typescript-eslint/no-explicit-any */
4
+ describe('RouteElement', () => {
5
+ beforeEach(() => {
6
+ if (!customElements.get(RouteElement.tag)) {
7
+ customElements.define(RouteElement.tag, RouteElement);
8
+ }
9
+ document.body.innerHTML = '';
10
+ });
11
+ describe('Attribute Accessors & toDefinition Logic', () => {
12
+ it.each([
13
+ {
14
+ it: 'returns default path "*" if attribute is missing',
15
+ html: `<wavelength-route component="my-view"></wavelength-route>`,
16
+ expectedPath: '*',
17
+ expectedTag: 'my-view'
18
+ },
19
+ {
20
+ it: 'extracts specified path and component tag',
21
+ html: `<wavelength-route path="/home" component="home-view"></wavelength-route>`,
22
+ expectedPath: '/home',
23
+ expectedTag: 'home-view'
24
+ },
25
+ {
26
+ it: 'returns null and warns if component attribute is missing',
27
+ html: `<wavelength-route path="/error"></wavelength-route>`,
28
+ expectedPath: '/error',
29
+ expectedTag: '',
30
+ shouldBeNull: true
31
+ }
32
+ ])('$it', ({ html, expectedPath, expectedTag, shouldBeNull }) => {
33
+ document.body.innerHTML = html;
34
+ const el = document.querySelector('wavelength-route');
35
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
36
+ expect(el.path).toBe(expectedPath);
37
+ expect(el.componentTag).toBe(expectedTag);
38
+ const def = el.toDefinition({ document });
39
+ if (shouldBeNull) {
40
+ expect(def).toBeNull();
41
+ expect(warnSpy).toHaveBeenCalled();
42
+ }
43
+ else {
44
+ expect(def?.path).toBe(expectedPath);
45
+ expect(def?.routeComponent).toBeDefined();
46
+ }
47
+ warnSpy.mockRestore();
48
+ });
49
+ });
50
+ describe('SSR / Environment Resilience', () => {
51
+ it('returns a no-op component if document is missing', () => {
52
+ const el = document.createElement('wavelength-route');
53
+ el.setAttribute('component', 'test-view');
54
+ const def = el.toDefinition({});
55
+ expect(def?.path).toBe('*');
56
+ expect(def?.routeComponent.mount).toBeInstanceOf(Function);
57
+ expect(() => def?.routeComponent.mount({}, {})).not.toThrow();
58
+ expect(() => def?.routeComponent.unmount?.()).not.toThrow();
59
+ });
60
+ });
61
+ describe('RouteComponent Mounting Logic', () => {
62
+ it('properly creates element and injects context on mount', () => {
63
+ const el = document.createElement('wavelength-route');
64
+ el.setAttribute('component', 'user-profile');
65
+ const def = el.toDefinition({ document });
66
+ const container = document.createElement('div');
67
+ const mockContext = { path: '/user/1', routeParams: { id: '1' }, query: new URLSearchParams() };
68
+ def?.routeComponent.mount(container, mockContext);
69
+ const renderedElement = container.querySelector('user-profile');
70
+ expect(renderedElement).toBeDefined();
71
+ expect(renderedElement.routeContext).toEqual(mockContext);
72
+ expect(def?.routeComponent.unmount).toBeDefined();
73
+ def?.routeComponent.unmount?.();
74
+ });
75
+ });
76
+ });
77
+ //# sourceMappingURL=RouteElement.test.js.map
File without changes
File without changes
File without changes
package/dist/index.js ADDED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes