@brightspace-ui/labs 2.13.0 → 2.14.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/package.json +3 -2
- package/src/lang/ar.js +61 -0
- package/src/lang/cy.js +61 -0
- package/src/lang/da.js +61 -0
- package/src/lang/de.js +61 -0
- package/src/lang/en-gb.js +61 -0
- package/src/lang/en.js +60 -0
- package/src/lang/es-es.js +61 -0
- package/src/lang/es.js +61 -0
- package/src/lang/fr-fr.js +61 -0
- package/src/lang/fr-on.js +61 -0
- package/src/lang/fr.js +61 -0
- package/src/lang/haw.js +61 -0
- package/src/lang/hi.js +61 -0
- package/src/lang/ja.js +61 -0
- package/src/lang/ko.js +61 -0
- package/src/lang/nl.js +61 -0
- package/src/lang/pt.js +61 -0
- package/src/lang/sv.js +61 -0
- package/src/lang/tr.js +61 -0
- package/src/lang/zh-cn.js +60 -0
- package/src/lang/zh-tw.js +61 -0
- package/src/utilities/router/README.md +297 -0
- package/src/utilities/router/RouteReactor.js +21 -0
- package/src/utilities/router/index.js +2 -0
- package/src/utilities/router/router.js +176 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# @brightspace-ui/labs/utilities/lit-router
|
|
2
|
+
|
|
3
|
+
A Lit wrapper around the [Page.js Router](https://visionmedia.github.io/page.js/).
|
|
4
|
+
|
|
5
|
+
The aim of this library is to provide an easy way to define routes, lazy load the view, and react to changes to the route.
|
|
6
|
+
|
|
7
|
+
## Usage
|
|
8
|
+
|
|
9
|
+
### Route Registration
|
|
10
|
+
|
|
11
|
+
Registering routes defines the routing table used when determining the view to render.
|
|
12
|
+
|
|
13
|
+
When the URL matches a particular route's pattern, the `view` function is called and returns a Lit `html` literal to render.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { registerRoutes } from '@brightspace-ui/labs/utilities/lit-router';
|
|
17
|
+
|
|
18
|
+
registerRoutes([
|
|
19
|
+
{
|
|
20
|
+
pattern: '/example',
|
|
21
|
+
loader: () => import('./home.js'),
|
|
22
|
+
view: () => html`<test-home></test-home>`
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: '/example/:id',
|
|
26
|
+
view: ctx => html`<test-id id=${ctx.params.id}></test-id>`
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: '/example/foo/:bar',
|
|
30
|
+
view: ctx => html`
|
|
31
|
+
<test-foo bar=${ctx.params.bar}>
|
|
32
|
+
${ctx.search.name}
|
|
33
|
+
</test-foo>
|
|
34
|
+
`
|
|
35
|
+
}
|
|
36
|
+
], options);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
#### Route Properties
|
|
40
|
+
|
|
41
|
+
Each route has the following properties:
|
|
42
|
+
- `pattern` (required): The Page.js route pattern on which to match
|
|
43
|
+
- `loader` (optional): Allows for lazy-loading dependencies (e.g. importing view files) before rendering the view; must return a Promise
|
|
44
|
+
- `view` (optional): Function that returns a Lit `html` literal to render
|
|
45
|
+
- `to` (optional): String indicating a redirect path, using Page.js `redirect(fromPath, toPath)`
|
|
46
|
+
|
|
47
|
+
#### View Context
|
|
48
|
+
|
|
49
|
+
The view is provided a context object containing:
|
|
50
|
+
- `params`: URL segment parameters (e.g. `:id`)
|
|
51
|
+
- `search`: search query values
|
|
52
|
+
- `options`: object passed by the entry-point
|
|
53
|
+
- `path`: Pathname and query string (e.g. `"/login?foo=bar"`)
|
|
54
|
+
- `pathname`: Pathname void of query string (e.g. `"/login"`)
|
|
55
|
+
- `hash`: URL hash (e.g. `"#hash=values"`)
|
|
56
|
+
- `route`: route pattern given to the view in the router
|
|
57
|
+
- `title`: title in the push state
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
```js
|
|
61
|
+
{
|
|
62
|
+
pattern: '/user/:id/:page' // search: ?semester=1
|
|
63
|
+
view: ctx => html`
|
|
64
|
+
<user-view
|
|
65
|
+
id=${ctx.options.id}
|
|
66
|
+
page=${ctx.params.page}
|
|
67
|
+
semester=${ctx.search.semester}>
|
|
68
|
+
</user-view>
|
|
69
|
+
`
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### Options
|
|
74
|
+
|
|
75
|
+
Options are the second parameter passed to `registerRoutes`. The two tables below encompasses all of the attributes that the options object can use.
|
|
76
|
+
|
|
77
|
+
Page.js options:
|
|
78
|
+
|
|
79
|
+
| Name | Description | Default |
|
|
80
|
+
| :------------------ | :---------------------------------------------------------------------: | ------: |
|
|
81
|
+
| click | bind to click events | true |
|
|
82
|
+
| popstate | bind to popstate | true |
|
|
83
|
+
| dispatch | perform initial dispatch | true |
|
|
84
|
+
| hashbang | add #! before urls | false |
|
|
85
|
+
| decodeURLComponents | remove URL encoding from path components (query string, pathname, hash) | true |
|
|
86
|
+
|
|
87
|
+
Additional options:
|
|
88
|
+
|
|
89
|
+
| Name | Description | Default |
|
|
90
|
+
| :--------- | :---------------------------------------------------: | ------: |
|
|
91
|
+
| basePath | the path all other paths are appended too | '/' |
|
|
92
|
+
| customPage | don't use the global page object (useful for testing) | false |
|
|
93
|
+
|
|
94
|
+
### Multiple Route Loaders
|
|
95
|
+
|
|
96
|
+
Some complex applications have many sub applications, and in these scenarios it may be beneficial to delegate to multiple route loaders.
|
|
97
|
+
|
|
98
|
+
Example directory structure:
|
|
99
|
+
```
|
|
100
|
+
/src
|
|
101
|
+
| /components
|
|
102
|
+
|
|
|
103
|
+
| /app1
|
|
104
|
+
| | app1-view.js
|
|
105
|
+
| | route-loader.js
|
|
106
|
+
|
|
|
107
|
+
| /app2
|
|
108
|
+
| | app2-view.js
|
|
109
|
+
| | route-loader.js
|
|
110
|
+
|
|
|
111
|
+
| entry-point.js
|
|
112
|
+
| route-loader.js
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The main route-loader in the root of the `src` directory should import the route-loader files in the subdirectories.
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
/* src/route-loader.js */
|
|
119
|
+
import { loader as app1Loader } from './app1/route-loader.js';
|
|
120
|
+
import { loader as app2Loader } from './app2/route-loader.js';
|
|
121
|
+
import { registerRoutes } from '@brightspace-ui/labs/utilities/lit-router';
|
|
122
|
+
|
|
123
|
+
registerRoutes([
|
|
124
|
+
{
|
|
125
|
+
pattern: '/',
|
|
126
|
+
view: () => html`<entry-point></entry-point>`
|
|
127
|
+
},
|
|
128
|
+
app1Loader,
|
|
129
|
+
app2Loader
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
/* src/page1/route-loader.js */
|
|
133
|
+
export const loader () => [
|
|
134
|
+
{
|
|
135
|
+
pattern: '/app1',
|
|
136
|
+
loader: () => import('./app1-view.js'),
|
|
137
|
+
view: () => html`<app-1></app-1>`
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### RouteReactor
|
|
143
|
+
|
|
144
|
+
The `RouteReactor` is a [Reactive Controller](https://lit.dev/docs/composition/controllers/) responsible for re-rendering the nested view when the route changes.
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { RouteReactor } from '@brightspace-ui/labs/utilities/lit-router';
|
|
148
|
+
|
|
149
|
+
class EntryPoint extends LitElement {
|
|
150
|
+
|
|
151
|
+
constructor() {
|
|
152
|
+
super();
|
|
153
|
+
this.route = new RouteReactor(this);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
render() {
|
|
157
|
+
// Options for the views. Can be used for attributes */
|
|
158
|
+
const options = { };
|
|
159
|
+
return this.route.renderView(options);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
A `RouteReactor` can also be used to react to changes to the URL. The available properties are the same as the context object passed to the views above.
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
import { RouteReactor } from '@brightspace-ui/labs/utilities/lit-router';
|
|
169
|
+
|
|
170
|
+
class FooBar extends LitElement {
|
|
171
|
+
|
|
172
|
+
constructor() {
|
|
173
|
+
super();
|
|
174
|
+
this.route = new RouteReactor(this);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
render() {
|
|
178
|
+
const userId = this.route.params.userId;
|
|
179
|
+
const orgId = this.route.search['org-unit'];
|
|
180
|
+
return html`<span> user: ${userId} orgUnit: ${orgId}</span>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Helpers
|
|
187
|
+
|
|
188
|
+
#### Navigating and Redirecting
|
|
189
|
+
|
|
190
|
+
Page.js will hook into any `<a>` tags and handle the navigation automatically. However, to navigate manually use `navigate(path)`:
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
import { navigate } from '@brightspace-ui/labs/utilities/lit-router';
|
|
194
|
+
|
|
195
|
+
navigate('/');
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
To programmatically redirect to a page and have the previous history item be replaced with the new one, use `redirect(path)`:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
import { redirect } from '@brightspace-ui/labs/utilities/lit-router';
|
|
202
|
+
|
|
203
|
+
redirect('/');
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Testing Routing in Your Application
|
|
207
|
+
|
|
208
|
+
For testing page routing in your application, this template is recommended:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
describe('Page Routing', () => {
|
|
212
|
+
|
|
213
|
+
beforeEach(async () => {
|
|
214
|
+
// Initialize the routes here or import a file
|
|
215
|
+
// that calls registerRoutes and expose a way to recall it
|
|
216
|
+
initRouter();
|
|
217
|
+
entryPoint = await fixture(html`<!-- Your ViewReactor component here -->`);
|
|
218
|
+
navigate('/'); // Reset tests back to the index, clears the url
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
afterEach(() => {
|
|
222
|
+
RouterTesting.reset(); // creates a new router instance, clears any router related reactive controllers.
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Your tests here
|
|
226
|
+
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Known Issues
|
|
231
|
+
|
|
232
|
+
### Route order inversion issue
|
|
233
|
+
|
|
234
|
+
There's currently an issue with the way registered routes are processed that can cause the order of matching to appear to be inverted. This is not noticeable in many cases since it's often the case that only a single route will match regardless of the order. This does however come up when dealing with wildcard (`*`) routes (e.g. 404 redirect routes).
|
|
235
|
+
|
|
236
|
+
If you notice that you have to put any routes with wildcards (`*`) before those without instead of after, this is the reason why.
|
|
237
|
+
|
|
238
|
+
#### Issue Fix
|
|
239
|
+
|
|
240
|
+
There is currently a fix available for this ordering issue, but it requires setting the `enableRouteOrderFix` option to `true`. Ex:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
registerRoutes([
|
|
244
|
+
{
|
|
245
|
+
pattern: '/home',
|
|
246
|
+
view: () => html`...`
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
pattern: '/404',
|
|
250
|
+
view: () => html`...`
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
pattern: '*',
|
|
254
|
+
to: '/404'
|
|
255
|
+
}
|
|
256
|
+
], {
|
|
257
|
+
enableRouteOrderFix: true
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Note: The reason this is an opt-in fix is that a lot of existing implementations of lit-router have worked around this issue by putting the wildcard routes at the start, which would break if the issue were fixed for everyone automatically.
|
|
262
|
+
|
|
263
|
+
## Developing and Contributing
|
|
264
|
+
|
|
265
|
+
After cloning the repo, run `npm install` to install dependencies.
|
|
266
|
+
|
|
267
|
+
### Testing
|
|
268
|
+
|
|
269
|
+
To run the full suite of tests:
|
|
270
|
+
|
|
271
|
+
```shell
|
|
272
|
+
npm test
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Alternatively, tests can be selectively run:
|
|
276
|
+
|
|
277
|
+
```shell
|
|
278
|
+
# eslint
|
|
279
|
+
npm run lint
|
|
280
|
+
|
|
281
|
+
# unit tests
|
|
282
|
+
npm run test:unit
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Running the demos
|
|
286
|
+
|
|
287
|
+
To start a [@web/dev-server](https://modern-web.dev/docs/dev-server/overview/) that hosts the demo pages and tests:
|
|
288
|
+
|
|
289
|
+
```shell
|
|
290
|
+
npm start
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Versioning and Releasing
|
|
294
|
+
|
|
295
|
+
This repo is configured to use `semantic-release`. Commits prefixed with `fix:` and `feat:` will trigger patch and minor releases when merged to `main`.
|
|
296
|
+
|
|
297
|
+
To learn how to create major releases and release from maintenance branches, refer to the [semantic-release GitHub Action](https://github.com/BrightspaceUI/actions/tree/main/semantic-release) documentation.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { _createReducedContext, ContextReactor } from './router.js';
|
|
2
|
+
|
|
3
|
+
export class RouteReactor extends ContextReactor {
|
|
4
|
+
constructor(host) {
|
|
5
|
+
super(host, ctx => {
|
|
6
|
+
const reduced = _createReducedContext(ctx);
|
|
7
|
+
|
|
8
|
+
Object.keys(reduced).forEach(ctxKey => {
|
|
9
|
+
this[ctxKey] = reduced[ctxKey];
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
this.renderView = opts => ctx.view?.(host, opts);
|
|
13
|
+
|
|
14
|
+
// this.path = ctx.pathname;
|
|
15
|
+
// this.params = ctx.params;
|
|
16
|
+
// this.search = ctx.searchParams;
|
|
17
|
+
});
|
|
18
|
+
this.renderView = () => {};
|
|
19
|
+
super.init();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import page from 'page';
|
|
2
|
+
|
|
3
|
+
let activePage = page;
|
|
4
|
+
let _lastOptions = {};
|
|
5
|
+
let _lastContext = {};
|
|
6
|
+
|
|
7
|
+
export const _createReducedContext = pageContext => ({
|
|
8
|
+
params: pageContext.params,
|
|
9
|
+
search: pageContext.searchParams,
|
|
10
|
+
path: pageContext.path,
|
|
11
|
+
pathname: pageContext.pathname,
|
|
12
|
+
hash: pageContext.hash,
|
|
13
|
+
route: pageContext.routePath,
|
|
14
|
+
title: pageContext.title,
|
|
15
|
+
options: {},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const _storeCtx = () => {
|
|
19
|
+
activePage('*', (context, next) => {
|
|
20
|
+
_lastContext = context;
|
|
21
|
+
next();
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const _handleRouteView = (context, next, r) => {
|
|
26
|
+
if (r.view) {
|
|
27
|
+
const reducedContext = _createReducedContext(context);
|
|
28
|
+
context.view = (host, options) => {
|
|
29
|
+
reducedContext.options = options || {};
|
|
30
|
+
return r.view.call(host, reducedContext);
|
|
31
|
+
};
|
|
32
|
+
context.handled = true;
|
|
33
|
+
|
|
34
|
+
next();
|
|
35
|
+
} else {
|
|
36
|
+
next();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const _handleRouteLoader = r => (context, next) => {
|
|
41
|
+
const enableRouteOrderFix = _lastOptions?.enableRouteOrderFix ?? false;
|
|
42
|
+
|
|
43
|
+
// Skip further pattern matches if the route has already been handled
|
|
44
|
+
if (enableRouteOrderFix && context.handled) {
|
|
45
|
+
next();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (r.loader) {
|
|
50
|
+
r.loader().then(() => {
|
|
51
|
+
_handleRouteView(context, next, r);
|
|
52
|
+
});
|
|
53
|
+
} else if (r.pattern && r.to) {
|
|
54
|
+
if (enableRouteOrderFix) {
|
|
55
|
+
activePage.redirect(r.to);
|
|
56
|
+
} else {
|
|
57
|
+
activePage.redirect(r.pattern, r.to);
|
|
58
|
+
next();
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
_handleRouteView(context, next, r);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const _registerRoute = r => {
|
|
66
|
+
activePage(r.pattern, _handleRouteLoader(r));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const _registerRoutes = routes => {
|
|
70
|
+
if (typeof routes === 'function') {
|
|
71
|
+
_registerRoutes(routes());
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
routes.forEach(r => {
|
|
75
|
+
if (typeof r === 'function') {
|
|
76
|
+
_registerRoutes(r());
|
|
77
|
+
} else {
|
|
78
|
+
_registerRoute(r);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const configure = options => {
|
|
84
|
+
if (options && options.customPage) activePage = page.create();
|
|
85
|
+
if (options && options.basePath) activePage.base(options.basePath);
|
|
86
|
+
activePage(options);
|
|
87
|
+
_lastOptions = options;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
let hasRegistered = false;
|
|
91
|
+
|
|
92
|
+
export const registerRoutes = (routes, options) => {
|
|
93
|
+
if (hasRegistered) throw new Error('May not construct multiple routers.');
|
|
94
|
+
hasRegistered = true;
|
|
95
|
+
|
|
96
|
+
if (!options?.enableRouteOrderFix) console.warn('lit-router: The enableRouteOrderFix option is not enabled. This may cause issues with route handling. See here for details: https://github.com/BrightspaceUILabs/router/blob/main/README.md#route-order-inversion-issue');
|
|
97
|
+
|
|
98
|
+
configure(options);
|
|
99
|
+
|
|
100
|
+
activePage('*', (context, next) => {
|
|
101
|
+
context.searchParams = {};
|
|
102
|
+
const searchParams = new URLSearchParams(context.querystring);
|
|
103
|
+
searchParams.forEach((value, key) => {
|
|
104
|
+
context.searchParams[key] = value;
|
|
105
|
+
});
|
|
106
|
+
next();
|
|
107
|
+
});
|
|
108
|
+
_registerRoutes(routes);
|
|
109
|
+
_storeCtx();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const addMiddleware = callback => {
|
|
113
|
+
activePage('*', (ctx, next) => {
|
|
114
|
+
callback(ctx);
|
|
115
|
+
next();
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Triggers navigation to the specified route path.
|
|
120
|
+
// Creates a new entry in the browser's history stack.
|
|
121
|
+
export const navigate = path => {
|
|
122
|
+
activePage.show(path);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Triggers navigation to the specified route path.
|
|
126
|
+
// Replaces the current entry in the browser's history stack.
|
|
127
|
+
export const redirect = path => {
|
|
128
|
+
activePage.redirect(path);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export class ContextReactor {
|
|
132
|
+
|
|
133
|
+
static reset() {
|
|
134
|
+
this.listeners = [];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
constructor(host, callback, initialize) {
|
|
138
|
+
ContextReactor.listeners.push({ host, callback });
|
|
139
|
+
|
|
140
|
+
if (ContextReactor.listeners.length === 1) {
|
|
141
|
+
addMiddleware(ctx => {
|
|
142
|
+
ContextReactor.listeners.forEach(listener => {
|
|
143
|
+
listener.callback(ctx);
|
|
144
|
+
// call requestUpdate only for known routes when ctx.handled is truthy
|
|
145
|
+
if (listener.host && ctx.handled) {
|
|
146
|
+
listener.host.requestUpdate();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// initialize the listener with the context from the last run
|
|
152
|
+
if (initialize) {
|
|
153
|
+
initialize(_lastContext);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
init() {
|
|
158
|
+
activePage();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
ContextReactor.listeners = [];
|
|
164
|
+
|
|
165
|
+
export const RouterTesting = {
|
|
166
|
+
reset: () => {
|
|
167
|
+
activePage.stop();
|
|
168
|
+
activePage = page.create();
|
|
169
|
+
hasRegistered = false;
|
|
170
|
+
ContextReactor.reset();
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
restart: () => {
|
|
174
|
+
activePage.start(_lastOptions);
|
|
175
|
+
},
|
|
176
|
+
};
|