@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.
- package/.gitlab-ci.yml +84 -0
- package/.husky/pre-commit +38 -0
- package/.storybook/main.ts +16 -0
- package/.storybook/manager.ts +7 -0
- package/.storybook/preview.ts +23 -0
- package/.storybook/theme.ts +30 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/README.md +556 -0
- package/dist/elements/RouteElement.d.ts +0 -0
- package/dist/elements/RouteElement.d.ts.map +0 -0
- package/dist/elements/RouteElement.js +0 -0
- package/dist/elements/RouteElement.js.map +0 -0
- package/dist/elements/RouterElement.d.ts +0 -0
- package/dist/elements/RouterElement.d.ts.map +0 -0
- package/dist/elements/RouterElement.js +0 -0
- package/dist/elements/RouterElement.js.map +0 -0
- package/dist/elements/__tests__/RouteElement.test.d.ts +0 -0
- package/dist/elements/__tests__/RouteElement.test.d.ts.map +0 -0
- package/dist/elements/__tests__/RouteElement.test.js +77 -0
- package/dist/elements/__tests__/RouteElement.test.js.map +0 -0
- package/dist/elements/__tests__/RouterElement.test.d.ts +0 -0
- package/dist/elements/__tests__/RouterElement.test.d.ts.map +0 -0
- package/dist/elements/__tests__/RouterElement.test.js +0 -0
- package/dist/elements/__tests__/RouterElement.test.js.map +0 -0
- package/dist/index.d.ts +0 -0
- package/dist/index.d.ts.map +0 -0
- package/dist/index.js +0 -0
- package/dist/index.js.map +0 -0
- package/dist/models/CompiledRoute.d.ts +0 -0
- package/dist/models/CompiledRoute.d.ts.map +0 -0
- package/dist/models/CompiledRoute.js +0 -0
- package/dist/models/CompiledRoute.js.map +0 -0
- package/dist/models/MatchedRoute.d.ts +0 -0
- package/dist/models/MatchedRoute.d.ts.map +0 -0
- package/dist/models/MatchedRoute.js +0 -0
- package/dist/models/MatchedRoute.js.map +0 -0
- package/dist/models/RouteComponent.d.ts +0 -0
- package/dist/models/RouteComponent.d.ts.map +0 -0
- package/dist/models/RouteComponent.js +0 -0
- package/dist/models/RouteComponent.js.map +0 -0
- package/dist/models/RouteContext.d.ts +0 -0
- package/dist/models/RouteContext.d.ts.map +0 -0
- package/dist/models/RouteContext.js +0 -0
- package/dist/models/RouteContext.js.map +0 -0
- package/dist/models/RouteDefinition.d.ts +0 -0
- package/dist/models/RouteDefinition.d.ts.map +0 -0
- package/dist/models/RouteDefinition.js +0 -0
- package/dist/models/RouteDefinition.js.map +0 -0
- package/dist/models/RouteGuard.d.ts +0 -0
- package/dist/models/RouteGuard.d.ts.map +0 -0
- package/dist/models/RouteGuard.js +0 -0
- package/dist/models/RouteGuard.js.map +0 -0
- package/dist/models/RouteParams.d.ts +0 -0
- package/dist/models/RouteParams.d.ts.map +0 -0
- package/dist/models/RouteParams.js +0 -0
- package/dist/models/RouteParams.js.map +0 -0
- package/dist/models/index.d.ts +0 -0
- package/dist/models/index.d.ts.map +0 -0
- package/dist/models/index.js +0 -0
- package/dist/models/index.js.map +0 -0
- package/dist/prettier.config.d.ts +0 -0
- package/dist/prettier.config.d.ts.map +0 -0
- package/dist/router/Router.d.ts +0 -0
- package/dist/router/Router.d.ts.map +0 -0
- package/dist/router/Router.js +0 -0
- package/dist/router/Router.js.map +0 -0
- package/dist/router/__tests__/Router.test.d.ts +0 -0
- package/dist/router/__tests__/Router.test.d.ts.map +0 -0
- package/dist/router/__tests__/Router.test.js +0 -0
- package/dist/router/__tests__/Router.test.js.map +0 -0
- package/dist/src/elements/RouteElement.d.ts +0 -0
- package/dist/src/elements/RouteElement.d.ts.map +0 -0
- package/dist/src/elements/RouterElement.d.ts +0 -0
- package/dist/src/elements/RouterElement.d.ts.map +0 -0
- package/dist/src/elements/__tests__/RouteElement.test.d.ts +0 -0
- package/dist/src/elements/__tests__/RouteElement.test.d.ts.map +0 -0
- package/dist/src/elements/__tests__/RouterElement.test.d.ts +0 -0
- package/dist/src/elements/__tests__/RouterElement.test.d.ts.map +0 -0
- package/dist/src/index.d.ts +0 -0
- package/dist/src/index.d.ts.map +0 -0
- package/dist/src/models/CompiledRoute.d.ts +0 -0
- package/dist/src/models/CompiledRoute.d.ts.map +0 -0
- package/dist/src/models/MatchedRoute.d.ts +0 -0
- package/dist/src/models/MatchedRoute.d.ts.map +0 -0
- package/dist/src/models/RouteComponent.d.ts +0 -0
- package/dist/src/models/RouteComponent.d.ts.map +0 -0
- package/dist/src/models/RouteContext.d.ts +0 -0
- package/dist/src/models/RouteContext.d.ts.map +0 -0
- package/dist/src/models/RouteDefinition.d.ts +0 -0
- package/dist/src/models/RouteDefinition.d.ts.map +0 -0
- package/dist/src/models/RouteGuard.d.ts +0 -0
- package/dist/src/models/RouteGuard.d.ts.map +0 -0
- package/dist/src/models/RouteParams.d.ts +0 -0
- package/dist/src/models/RouteParams.d.ts.map +0 -0
- package/dist/src/models/index.d.ts +0 -0
- package/dist/src/models/index.d.ts.map +0 -0
- package/dist/src/router/Router.d.ts +0 -0
- package/dist/src/router/Router.d.ts.map +0 -0
- package/dist/src/router/__tests__/Router.test.d.ts +0 -0
- package/dist/src/router/__tests__/Router.test.d.ts.map +0 -0
- package/dist/src/stories/RouterElement.stories.d.ts +0 -0
- package/dist/src/stories/RouterElement.stories.d.ts.map +0 -0
- package/dist/src/stories/RouterElement.stories.test.d.ts +0 -0
- package/dist/src/stories/RouterElement.stories.test.d.ts.map +0 -0
- package/dist/src/utils/__tests__/compileRoute.test.d.ts +0 -0
- package/dist/src/utils/__tests__/compileRoute.test.d.ts.map +0 -0
- package/dist/src/utils/__tests__/matchRoute.test.d.ts +0 -0
- package/dist/src/utils/__tests__/matchRoute.test.d.ts.map +0 -0
- package/dist/src/utils/compileRoute.d.ts +0 -0
- package/dist/src/utils/compileRoute.d.ts.map +0 -0
- package/dist/src/utils/index.d.ts +0 -0
- package/dist/src/utils/index.d.ts.map +0 -0
- package/dist/src/utils/matchRoute.d.ts +0 -0
- package/dist/src/utils/matchRoute.d.ts.map +0 -0
- package/dist/stories/RouterElement.stories.d.ts +0 -0
- package/dist/stories/RouterElement.stories.d.ts.map +0 -0
- package/dist/stories/RouterElement.stories.js +0 -0
- package/dist/stories/RouterElement.stories.js.map +0 -0
- package/dist/utils/__tests__/compileRoute.test.d.ts +0 -0
- package/dist/utils/__tests__/compileRoute.test.d.ts.map +0 -0
- package/dist/utils/__tests__/compileRoute.test.js +0 -0
- package/dist/utils/__tests__/compileRoute.test.js.map +0 -0
- package/dist/utils/__tests__/matchRoute.test.d.ts +0 -0
- package/dist/utils/__tests__/matchRoute.test.d.ts.map +0 -0
- package/dist/utils/__tests__/matchRoute.test.js +0 -0
- package/dist/utils/__tests__/matchRoute.test.js.map +0 -0
- package/dist/utils/compileRoute.d.ts +0 -0
- package/dist/utils/compileRoute.d.ts.map +0 -0
- package/dist/utils/compileRoute.js +0 -0
- package/dist/utils/compileRoute.js.map +0 -0
- package/dist/utils/index.d.ts +0 -0
- package/dist/utils/index.d.ts.map +0 -0
- package/dist/utils/index.js +0 -0
- package/dist/utils/index.js.map +0 -0
- package/dist/utils/matchRoute.d.ts +0 -0
- package/dist/utils/matchRoute.d.ts.map +0 -0
- package/dist/utils/matchRoute.js +0 -0
- package/dist/utils/matchRoute.js.map +0 -0
- package/eslint.config.mjs +89 -0
- package/index.ts +15 -0
- package/package.json +54 -0
- package/prettier.config.ts +30 -0
- package/src/elements/RouteElement.ts +67 -0
- package/src/elements/RouterElement.ts +97 -0
- package/src/elements/__tests__/RouteElement.test.ts +89 -0
- package/src/elements/__tests__/RouterElement.test.ts +253 -0
- package/src/index.ts +15 -0
- package/src/models/CompiledRoute.ts +7 -0
- package/src/models/MatchedRoute.ts +7 -0
- package/src/models/RouteComponent.ts +6 -0
- package/src/models/RouteContext.ts +7 -0
- package/src/models/RouteDefinition.ts +6 -0
- package/src/models/RouteGuard.ts +6 -0
- package/src/models/RouteParams.ts +3 -0
- package/src/models/index.ts +17 -0
- package/src/router/Router.ts +225 -0
- package/src/router/__tests__/Router.test.ts +464 -0
- package/src/stories/RouterElement.stories.test.ts +310 -0
- package/src/stories/RouterElement.stories.ts +208 -0
- package/src/utils/__tests__/compileRoute.test.ts +97 -0
- package/src/utils/__tests__/matchRoute.test.ts +180 -0
- package/src/utils/compileRoute.ts +37 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/matchRoute.ts +37 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +55 -0
- package/vitest.config.ts +34 -0
- package/vitest.setup.ts +3 -0
- 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
|
|
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
|
|
File without changes
|
|
File without changes
|
package/dist/index.d.ts
ADDED
|
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
|