@excom/routable-element 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/routable-element.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/routable-element.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/routable-element.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_docs/all.log +1 -0
- package/.rush/temp/operation/build_docs/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_docs/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +96 -0
- package/package.json +45 -0
- package/rush-logs/routable-element.apply-exports.cache.log +1 -0
- package/rush-logs/routable-element.apply-exports.log +1 -0
- package/rush-logs/routable-element.build_docs.cache.log +1 -0
- package/rush-logs/routable-element.build_docs.log +1 -0
- package/rush-logs/routable-element.build_package-metas.cache.log +1 -0
- package/rush-logs/routable-element.build_package-metas.log +1 -0
- package/support/custom-elements.json +100 -0
- package/support/dist-docs/routable-element.md +99 -0
- package/support/docs/README.md +69 -0
- package/support/package-meta.json +73 -0
- package/support/tests/routable-element-routing.test.ts +243 -0
- package/support/tests/routable-element.test.ts +42 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: cd \"$RUSH_PROJECT_FOLDER\" && node ../heft-rig/scripts/apply-exports.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs \n"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"kind":"O","text":"Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs \n"}
|
package/config/rig.json
ADDED
package/index.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { KitRoute, kitRouter } from "@excom/kit-router";
|
|
2
|
+
import { Neutron } from "@excom/neutron";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composition base that makes an element route-aware. Subclasses implement `routeChanged` to react when the URL matches. Used by `<spa-route>`, `<spa-a>`, and `<spa-manager>`.
|
|
6
|
+
*
|
|
7
|
+
* @summary URL matching for Neutron elements.
|
|
8
|
+
*/
|
|
9
|
+
export const RoutableElement = Neutron({
|
|
10
|
+
tag: "noop-tag",
|
|
11
|
+
props: {
|
|
12
|
+
/**
|
|
13
|
+
* @option
|
|
14
|
+
* URL pattern to match. Supports named placeholders (`/users/:id`) and wildcards. Set this or `route-regex`.
|
|
15
|
+
* @values <path pattern>
|
|
16
|
+
*/
|
|
17
|
+
routeHref: String,
|
|
18
|
+
/**
|
|
19
|
+
* @option
|
|
20
|
+
* RegExp source string matched against the current URL — alternative to `route-href` for catch-alls / advanced patterns.
|
|
21
|
+
* @values <RegExp source>
|
|
22
|
+
*/
|
|
23
|
+
routeRegex: String,
|
|
24
|
+
/**
|
|
25
|
+
* @option
|
|
26
|
+
* Also match nested paths of `route-href` (e.g. `/users` stays active on `/users/42`). Essential for layout routes and nested SPAs.
|
|
27
|
+
*/
|
|
28
|
+
matchNested: Boolean,
|
|
29
|
+
routeInstance: KitRoute,
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
.defineMethods({
|
|
33
|
+
routeChanged: () => {
|
|
34
|
+
throw new Error("routeChanged must be implemented");
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
.onConnected(
|
|
38
|
+
({
|
|
39
|
+
routeHref,
|
|
40
|
+
routeRegex,
|
|
41
|
+
routeChanged,
|
|
42
|
+
matchNested,
|
|
43
|
+
isMoving,
|
|
44
|
+
wasMounted,
|
|
45
|
+
}) => {
|
|
46
|
+
if (isMoving) return;
|
|
47
|
+
return (
|
|
48
|
+
wasMounted &&
|
|
49
|
+
(routeHref || routeRegex) && {
|
|
50
|
+
routeInstance: new KitRoute(
|
|
51
|
+
(routeHref || new RegExp(routeRegex!))!,
|
|
52
|
+
routeChanged,
|
|
53
|
+
{
|
|
54
|
+
matchNested,
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
_usedRouteProp: "routeHref",
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
.onDisconnected(({ routeHref, isMoving }) => {
|
|
63
|
+
if (isMoving) return;
|
|
64
|
+
return (
|
|
65
|
+
routeHref && {
|
|
66
|
+
routeInstance: null,
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
})
|
|
70
|
+
.onPropChanged(
|
|
71
|
+
["routeHref", "routeRegex"],
|
|
72
|
+
({ routeHref, routeRegex, routeChanged, matchNested }, previous) => {
|
|
73
|
+
let routeInstance;
|
|
74
|
+
if (previous?.routeHref || previous?.routeRegex) {
|
|
75
|
+
// if previous routeInstance was set, remove it
|
|
76
|
+
routeInstance = null;
|
|
77
|
+
}
|
|
78
|
+
if (routeHref || routeRegex) {
|
|
79
|
+
// if new route, set a new routeInstance, which will also remove the previous one
|
|
80
|
+
routeInstance = new KitRoute(
|
|
81
|
+
(routeHref || new RegExp(routeRegex!))!,
|
|
82
|
+
routeChanged,
|
|
83
|
+
{ matchNested }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return { routeInstance };
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
.onPropChanged("routeInstance", ({ routeInstance }, previous) => {
|
|
90
|
+
if (previous?.routeInstance) {
|
|
91
|
+
kitRouter.off(previous.routeInstance);
|
|
92
|
+
}
|
|
93
|
+
if (routeInstance) {
|
|
94
|
+
kitRouter.on(routeInstance);
|
|
95
|
+
}
|
|
96
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@excom/routable-element",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "RoutableElement base for Neutron elements",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=24.13.0"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@excom/kit-router": "^0.1.0",
|
|
12
|
+
"@excom/neutron": "^0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@excom/heft-rig": "^0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"url": "excom-dev/nucleus",
|
|
20
|
+
"directory": "packages/routable-element"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/excom-dev/nucleus/tree/main/packages/routable-element/support/docs/README.md",
|
|
23
|
+
"bugs": "https://github.com/excom-dev/nucleus/issues",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"RoutableElement",
|
|
26
|
+
"routable-element",
|
|
27
|
+
"neutron",
|
|
28
|
+
"kit-element-base",
|
|
29
|
+
"custom-elements"
|
|
30
|
+
],
|
|
31
|
+
"excom": {
|
|
32
|
+
"packageType": "element-base"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node node_modules/@excom/heft-rig/scripts/vite-build.mjs",
|
|
36
|
+
"build:watch": "node node_modules/@excom/heft-rig/scripts/vite-build-watch.mjs",
|
|
37
|
+
"format": "node node_modules/@excom/heft-rig/scripts/format.mjs",
|
|
38
|
+
"test": "node node_modules/@excom/heft-rig/scripts/vitest.mjs",
|
|
39
|
+
"coverage": "node node_modules/@excom/heft-rig/scripts/coverage.mjs",
|
|
40
|
+
"dev": "node node_modules/@excom/heft-rig/scripts/vite-dev.mjs",
|
|
41
|
+
"preview": "node node_modules/@excom/heft-rig/scripts/vite-preview.mjs",
|
|
42
|
+
"build:package-metas": "node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs",
|
|
43
|
+
"build:docs": "node node_modules/@excom/heft-rig/scripts/build-docs.mjs"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Caching has been disabled for this project's "apply-exports" command.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: cd "$RUSH_PROJECT_FOLDER" && node ../heft-rig/scripts/apply-exports.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:docs" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-docs.mjs
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This project does not define the caching behavior of the "build:package-metas" command, so caching has been disabled.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Invoking: node node_modules/@excom/heft-rig/scripts/build-package-metas.mjs
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"modules": [
|
|
4
|
+
{
|
|
5
|
+
"kind": "javascript-module",
|
|
6
|
+
"path": "index.ts",
|
|
7
|
+
"declarations": [
|
|
8
|
+
{
|
|
9
|
+
"kind": "mixin",
|
|
10
|
+
"name": "RoutableElement",
|
|
11
|
+
"summary": "URL matching for Neutron elements.",
|
|
12
|
+
"description": "Composition base that makes an element route-aware. Subclasses implement `routeChanged` to react when the URL matches. Used by `<spa-route>`, `<spa-a>`, and `<spa-manager>`.",
|
|
13
|
+
"attributes": [
|
|
14
|
+
{
|
|
15
|
+
"name": "route-href",
|
|
16
|
+
"type": {
|
|
17
|
+
"text": "string"
|
|
18
|
+
},
|
|
19
|
+
"description": "URL pattern to match. Supports named placeholders (`/users/:id`) and wildcards. Set this or `route-regex`.",
|
|
20
|
+
"fieldName": "routeHref",
|
|
21
|
+
"values": [
|
|
22
|
+
"<path pattern>"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "route-regex",
|
|
27
|
+
"type": {
|
|
28
|
+
"text": "string"
|
|
29
|
+
},
|
|
30
|
+
"description": "RegExp source string matched against the current URL — alternative to `route-href` for catch-alls / advanced patterns.",
|
|
31
|
+
"fieldName": "routeRegex",
|
|
32
|
+
"values": [
|
|
33
|
+
"<RegExp source>"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "match-nested",
|
|
38
|
+
"type": {
|
|
39
|
+
"text": "boolean"
|
|
40
|
+
},
|
|
41
|
+
"description": "Also match nested paths of `route-href` (e.g. `/users` stays active on `/users/42`). Essential for layout routes and nested SPAs.",
|
|
42
|
+
"fieldName": "matchNested"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"members": [
|
|
46
|
+
{
|
|
47
|
+
"kind": "field",
|
|
48
|
+
"name": "routeHref",
|
|
49
|
+
"type": {
|
|
50
|
+
"text": "string"
|
|
51
|
+
},
|
|
52
|
+
"privacy": "public",
|
|
53
|
+
"readonly": false,
|
|
54
|
+
"description": "URL pattern to match. Supports named placeholders (`/users/:id`) and wildcards. Set this or `route-regex`.",
|
|
55
|
+
"_neutron": {
|
|
56
|
+
"surface": "option"
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"kind": "field",
|
|
61
|
+
"name": "routeRegex",
|
|
62
|
+
"type": {
|
|
63
|
+
"text": "string"
|
|
64
|
+
},
|
|
65
|
+
"privacy": "public",
|
|
66
|
+
"readonly": false,
|
|
67
|
+
"description": "RegExp source string matched against the current URL — alternative to `route-href` for catch-alls / advanced patterns.",
|
|
68
|
+
"_neutron": {
|
|
69
|
+
"surface": "option"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"kind": "field",
|
|
74
|
+
"name": "matchNested",
|
|
75
|
+
"type": {
|
|
76
|
+
"text": "boolean"
|
|
77
|
+
},
|
|
78
|
+
"privacy": "public",
|
|
79
|
+
"readonly": false,
|
|
80
|
+
"description": "Also match nested paths of `route-href` (e.g. `/users` stays active on `/users/42`). Essential for layout routes and nested SPAs.",
|
|
81
|
+
"_neutron": {
|
|
82
|
+
"surface": "option"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"exports": [
|
|
89
|
+
{
|
|
90
|
+
"kind": "js",
|
|
91
|
+
"name": "RoutableElement",
|
|
92
|
+
"declaration": {
|
|
93
|
+
"name": "RoutableElement",
|
|
94
|
+
"module": "index.ts"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# routable-element
|
|
2
|
+
|
|
3
|
+
URL matching for custom elements — give any Neutron class a
|
|
4
|
+
`route-href` and react when the address bar changes.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Path patterns** Named params (`/users/:id`) and wildcards
|
|
9
|
+
- **Regex routes** Full control via `route-regex`
|
|
10
|
+
- **Nested match** Keep layouts active under child paths
|
|
11
|
+
- **Live updates** Rematch when `route-href` / `route-regex` change
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
`@excom/routable-element` v0.1.0
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @excom/routable-element
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @excom/routable-element
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
yarn add @excom/routable-element
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Import
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { /* … */ } from "@excom/routable-element";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
Compose `RoutableElement` and implement `routeChanged`. Concrete
|
|
41
|
+
consumers include `<spa-route>`, `<spa-a>`, and `<spa-manager>`.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Neutron } from "@excom/neutron";
|
|
45
|
+
import { RoutableElement } from "@excom/routable-element";
|
|
46
|
+
|
|
47
|
+
export const PathAware = Neutron.compose([
|
|
48
|
+
RoutableElement,
|
|
49
|
+
Neutron({
|
|
50
|
+
tag: "path-aware",
|
|
51
|
+
props: {
|
|
52
|
+
isActive: Boolean,
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
])
|
|
56
|
+
.defineMethods({
|
|
57
|
+
routeChanged: (_el, { match }) => ({ isActive: !!match }),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
PathAware.define();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```html
|
|
64
|
+
<path-aware route-href="/dashboard" match-nested></path-aware>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### API Reference
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
#### Attributes
|
|
71
|
+
|
|
72
|
+
| Name | Surface | Type | Default | Values | Description |
|
|
73
|
+
| --- | --- | --- | --- | --- | --- |
|
|
74
|
+
| `route-href` | option | `string` | | `<path pattern>` | URL pattern to match. Supports named placeholders (`/users/:id`) and wildcards. Set this or `route-regex`. |
|
|
75
|
+
| `route-regex` | option | `string` | | `<RegExp source>` | RegExp source string matched against the current URL — alternative to `route-href` for catch-alls / advanced patterns. |
|
|
76
|
+
| `match-nested` | option | `boolean` | | | Also match nested paths of `route-href` (e.g. `/users` stays active on `/users/42`). Essential for layout routes and nested SPAs. |
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
### Examples
|
|
81
|
+
|
|
82
|
+
#### Path match & nested layouts
|
|
83
|
+
|
|
84
|
+
`route-href` activates on exact match; `match-nested` keeps a layout
|
|
85
|
+
mounted under child paths.
|
|
86
|
+
|
|
87
|
+
```html
|
|
88
|
+
<spa-route route-href="/users" match-nested>
|
|
89
|
+
<template>…layout…</template>
|
|
90
|
+
</spa-route>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### Catch-all via regex
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<spa-route route-regex=".*" is-fallback>
|
|
97
|
+
<template>404</template>
|
|
98
|
+
</spa-route>
|
|
99
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# routable-element
|
|
2
|
+
|
|
3
|
+
URL matching for custom elements — give any Neutron class a
|
|
4
|
+
`route-href` and react when the address bar changes.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
- **Path patterns** Named params (`/users/:id`) and wildcards
|
|
9
|
+
- **Regex routes** Full control via `route-regex`
|
|
10
|
+
- **Nested match** Keep layouts active under child paths
|
|
11
|
+
- **Live updates** Rematch when `route-href` / `route-regex` change
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
<include-content is-active template-ref="/views/install-section/install-section.html"></include-content>
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
Compose `RoutableElement` and implement `routeChanged`. Concrete
|
|
20
|
+
consumers include `<spa-route>`, `<spa-a>`, and `<spa-manager>`.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Neutron } from "@excom/neutron";
|
|
24
|
+
import { RoutableElement } from "@excom/routable-element";
|
|
25
|
+
|
|
26
|
+
export const PathAware = Neutron.compose([
|
|
27
|
+
RoutableElement,
|
|
28
|
+
Neutron({
|
|
29
|
+
tag: "path-aware",
|
|
30
|
+
props: {
|
|
31
|
+
isActive: Boolean,
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
])
|
|
35
|
+
.defineMethods({
|
|
36
|
+
routeChanged: (_el, { match }) => ({ isActive: !!match }),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
PathAware.define();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<path-aware route-href="/dashboard" match-nested></path-aware>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### API Reference
|
|
47
|
+
|
|
48
|
+
<include-content is-active template-ref="/views/api-reference/api-reference.html"></include-content>
|
|
49
|
+
|
|
50
|
+
### Examples
|
|
51
|
+
|
|
52
|
+
#### Path match & nested layouts
|
|
53
|
+
|
|
54
|
+
`route-href` activates on exact match; `match-nested` keeps a layout
|
|
55
|
+
mounted under child paths.
|
|
56
|
+
|
|
57
|
+
```html
|
|
58
|
+
<spa-route route-href="/users" match-nested>
|
|
59
|
+
<template>…layout…</template>
|
|
60
|
+
</spa-route>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Catch-all via regex
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<spa-route route-regex=".*" is-fallback>
|
|
67
|
+
<template>404</template>
|
|
68
|
+
</spa-route>
|
|
69
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"shortName": "routable-element",
|
|
3
|
+
"package": {
|
|
4
|
+
"name": "@excom/routable-element",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"description": "RoutableElement base for Neutron elements",
|
|
7
|
+
"peerDependencies": {},
|
|
8
|
+
"excom": {
|
|
9
|
+
"packageType": "element-base"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"demos": {},
|
|
13
|
+
"readme": "<h1 id=\"md-routable-element\">routable-element</h1>\n<p>URL matching for custom elements — give any Neutron class a\n<code>route-href</code> and react when the address bar changes.</p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Path patterns</strong> Named params (<code>/users/:id</code>) and wildcards</li>\n<li><strong>Regex routes</strong> Full control via <code>route-regex</code></li>\n<li><strong>Nested match</strong> Keep layouts active under child paths</li>\n<li><strong>Live updates</strong> Rematch when <code>route-href</code> / <code>route-regex</code> change</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Compose <code>RoutableElement</code> and implement <code>routeChanged</code>. Concrete\nconsumers include <code><spa-route></code>, <code><spa-a></code>, and <code><spa-manager></code>.</p>\n<include-content data-language=\"ts\"><template>import { Neutron } from \"@excom/neutron\";\nimport { RoutableElement } from \"@excom/routable-element\";\n\nexport const PathAware = Neutron.compose([\n RoutableElement,\n Neutron({\n tag: \"path-aware\",\n props: {\n isActive: Boolean,\n },\n }),\n])\n .defineMethods({\n routeChanged: (_el, { match }) => ({ isActive: !!match }),\n });\n\nPathAware.define();</template></include-content>\n<include-content data-language=\"html\"><template><path-aware route-href=\"/dashboard\" match-nested></path-aware></template></include-content>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-path-match-amp-nested-layouts\">Path match & nested layouts</h4>\n<p><code>route-href</code> activates on exact match; <code>match-nested</code> keeps a layout\nmounted under child paths.</p>\n<include-content data-language=\"html\"><template><spa-route route-href=\"/users\" match-nested>\n <template>…layout…</template>\n</spa-route></template></include-content>\n<h4 id=\"md-catch-all-via-regex\">Catch-all via regex</h4>\n<include-content data-language=\"html\"><template><spa-route route-regex=\".*\" is-fallback>\n <template>404</template>\n</spa-route></template></include-content>\n",
|
|
14
|
+
"docs": {
|
|
15
|
+
"readme": "<h1 id=\"md-routable-element\">routable-element</h1>\n<p>URL matching for custom elements — give any Neutron class a\n<code>route-href</code> and react when the address bar changes.</p>\n<h2 id=\"md-features\">Features</h2>\n<ul>\n<li><strong>Path patterns</strong> Named params (<code>/users/:id</code>) and wildcards</li>\n<li><strong>Regex routes</strong> Full control via <code>route-regex</code></li>\n<li><strong>Nested match</strong> Keep layouts active under child paths</li>\n<li><strong>Live updates</strong> Rematch when <code>route-href</code> / <code>route-regex</code> change</li>\n</ul>\n<h2 id=\"md-installation\">Installation</h2>\n<p><include-content is-active template-ref=\"/views/install-section/install-section.html\"></include-content></p>\n<h2 id=\"md-usage\">Usage</h2>\n<p>Compose <code>RoutableElement</code> and implement <code>routeChanged</code>. Concrete\nconsumers include <code><spa-route></code>, <code><spa-a></code>, and <code><spa-manager></code>.</p>\n<include-content data-language=\"ts\"><template>import { Neutron } from \"@excom/neutron\";\nimport { RoutableElement } from \"@excom/routable-element\";\n\nexport const PathAware = Neutron.compose([\n RoutableElement,\n Neutron({\n tag: \"path-aware\",\n props: {\n isActive: Boolean,\n },\n }),\n])\n .defineMethods({\n routeChanged: (_el, { match }) => ({ isActive: !!match }),\n });\n\nPathAware.define();</template></include-content>\n<include-content data-language=\"html\"><template><path-aware route-href=\"/dashboard\" match-nested></path-aware></template></include-content>\n<h3 id=\"md-api-reference\">API Reference</h3>\n<p><include-content is-active template-ref=\"/views/api-reference/api-reference.html\"></include-content></p>\n<h3 id=\"md-examples\">Examples</h3>\n<h4 id=\"md-path-match-amp-nested-layouts\">Path match & nested layouts</h4>\n<p><code>route-href</code> activates on exact match; <code>match-nested</code> keeps a layout\nmounted under child paths.</p>\n<include-content data-language=\"html\"><template><spa-route route-href=\"/users\" match-nested>\n <template>…layout…</template>\n</spa-route></template></include-content>\n<h4 id=\"md-catch-all-via-regex\">Catch-all via regex</h4>\n<include-content data-language=\"html\"><template><spa-route route-regex=\".*\" is-fallback>\n <template>404</template>\n</spa-route></template></include-content>\n"
|
|
16
|
+
},
|
|
17
|
+
"installation": {
|
|
18
|
+
"name": "@excom/routable-element",
|
|
19
|
+
"shortName": "routable-element",
|
|
20
|
+
"version": "0.1.0",
|
|
21
|
+
"description": "RoutableElement base for Neutron elements",
|
|
22
|
+
"packageType": "element-base",
|
|
23
|
+
"install": {
|
|
24
|
+
"npm": "npm install @excom/routable-element"
|
|
25
|
+
},
|
|
26
|
+
"imports": {
|
|
27
|
+
"js": "import { /* … */ } from \"@excom/routable-element\";"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": []
|
|
30
|
+
},
|
|
31
|
+
"elementApis": [
|
|
32
|
+
{
|
|
33
|
+
"summary": "URL matching for Neutron elements.",
|
|
34
|
+
"kind": "mixin",
|
|
35
|
+
"attributes": [
|
|
36
|
+
{
|
|
37
|
+
"name": "match-nested",
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"description": "Also match nested paths of <code>route-href</code> (e.g. <code>/users</code> stays active on <code>/users/42</code>). Essential for layout routes and nested SPAs.",
|
|
40
|
+
"fieldName": "matchNested",
|
|
41
|
+
"surface": "option"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "route-href",
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "URL pattern to match. Supports named placeholders (<code>/users/:id</code>) and wildcards. Set this or <code>route-regex</code>.",
|
|
47
|
+
"fieldName": "routeHref",
|
|
48
|
+
"surface": "option",
|
|
49
|
+
"values": "<path pattern>"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "route-regex",
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "RegExp source string matched against the current URL — alternative to <code>route-href</code> for catch-alls / advanced patterns.",
|
|
55
|
+
"fieldName": "routeRegex",
|
|
56
|
+
"surface": "option",
|
|
57
|
+
"values": "<RegExp source>"
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"events": [],
|
|
61
|
+
"slots": [],
|
|
62
|
+
"cssProperties": [],
|
|
63
|
+
"cssClasses": [],
|
|
64
|
+
"cssAliases": [],
|
|
65
|
+
"listens": [],
|
|
66
|
+
"commands": [],
|
|
67
|
+
"defaultActions": [],
|
|
68
|
+
"expectedChildren": [],
|
|
69
|
+
"provisions": []
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"exportedFiles": {}
|
|
73
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { RoutableElement } from "../../index";
|
|
2
|
+
import { Neutron } from "@excom/neutron";
|
|
3
|
+
import { KitRoute, kitRouter } from "@excom/kit-router";
|
|
4
|
+
import {
|
|
5
|
+
afterEach,
|
|
6
|
+
beforeEach,
|
|
7
|
+
describe,
|
|
8
|
+
expect,
|
|
9
|
+
fixture,
|
|
10
|
+
it,
|
|
11
|
+
vi,
|
|
12
|
+
wait,
|
|
13
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
14
|
+
|
|
15
|
+
const TAG = "routable-routing-test";
|
|
16
|
+
|
|
17
|
+
// Records every `routeChanged` call so tests can assert on router payloads.
|
|
18
|
+
const routeChangedSpy = vi.fn();
|
|
19
|
+
|
|
20
|
+
const PathAware = Neutron.compose([
|
|
21
|
+
RoutableElement,
|
|
22
|
+
Neutron({
|
|
23
|
+
tag: TAG,
|
|
24
|
+
props: {
|
|
25
|
+
isActive: Boolean,
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
]).defineMethods({
|
|
29
|
+
routeChanged: (element, data) => {
|
|
30
|
+
routeChangedSpy(element, data);
|
|
31
|
+
return { isActive: !!data.match };
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!customElements.get(TAG)) {
|
|
36
|
+
PathAware.define();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type TestElement = HTMLElement & {
|
|
40
|
+
routeHref: string | null;
|
|
41
|
+
routeRegex: string | null;
|
|
42
|
+
matchNested: boolean;
|
|
43
|
+
routeInstance: KitRoute | null;
|
|
44
|
+
isActive: boolean;
|
|
45
|
+
isMoving: boolean;
|
|
46
|
+
wasMounted: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
describe("RoutableElement routing", () => {
|
|
50
|
+
let onSpy: ReturnType<typeof vi.spyOn>;
|
|
51
|
+
let offSpy: ReturnType<typeof vi.spyOn>;
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
history.replaceState(null, "", "/");
|
|
55
|
+
routeChangedSpy.mockClear();
|
|
56
|
+
onSpy = vi.spyOn(kitRouter, "on");
|
|
57
|
+
offSpy = vi.spyOn(kitRouter, "off");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
document.body.innerHTML = "";
|
|
62
|
+
vi.restoreAllMocks();
|
|
63
|
+
history.replaceState(null, "", "/");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("registers a path route on connect and reacts to URL changes", async () => {
|
|
67
|
+
const el = fixture<TestElement>(
|
|
68
|
+
`<${TAG} route-href="/users/:id"></${TAG}>`
|
|
69
|
+
);
|
|
70
|
+
await wait(0);
|
|
71
|
+
expect(el.routeInstance).toBeInstanceOf(KitRoute);
|
|
72
|
+
expect(el.routeInstance!.key).toBe("/users/:id");
|
|
73
|
+
expect(onSpy).toHaveBeenCalledWith(el.routeInstance);
|
|
74
|
+
// registering immediately reports the current (non-matching) URL
|
|
75
|
+
expect(routeChangedSpy).toHaveBeenLastCalledWith(
|
|
76
|
+
el,
|
|
77
|
+
expect.objectContaining({ match: null, params: null })
|
|
78
|
+
);
|
|
79
|
+
expect(el.isActive).toBe(false);
|
|
80
|
+
|
|
81
|
+
kitRouter.pushState({ url: "/users/7" });
|
|
82
|
+
expect(routeChangedSpy).toHaveBeenLastCalledWith(
|
|
83
|
+
el,
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
params: { id: "7" },
|
|
86
|
+
match: expect.arrayContaining(["/users/7", "7"]),
|
|
87
|
+
move: "push",
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
expect(el.isActive).toBe(true);
|
|
91
|
+
|
|
92
|
+
kitRouter.pushState({ url: "/elsewhere" });
|
|
93
|
+
expect(el.isActive).toBe(false);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("does not register a route when neither route-href nor route-regex is set", async () => {
|
|
97
|
+
const el = fixture<TestElement>(`<${TAG}></${TAG}>`);
|
|
98
|
+
await wait(0);
|
|
99
|
+
expect(el.routeInstance ?? null).toBeNull();
|
|
100
|
+
expect(onSpy).not.toHaveBeenCalled();
|
|
101
|
+
expect(routeChangedSpy).not.toHaveBeenCalled();
|
|
102
|
+
el.remove();
|
|
103
|
+
await wait(0);
|
|
104
|
+
expect(offSpy).not.toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("registers a regex route", async () => {
|
|
108
|
+
const el = fixture<TestElement>(
|
|
109
|
+
`<${TAG} route-regex="^/docs(/.*)?$"></${TAG}>`
|
|
110
|
+
);
|
|
111
|
+
await wait(0);
|
|
112
|
+
expect(el.routeInstance!.key).toBeInstanceOf(RegExp);
|
|
113
|
+
expect(el.routeInstance!.regex.source).toBe(
|
|
114
|
+
new RegExp("^/docs(/.*)?$").source
|
|
115
|
+
);
|
|
116
|
+
expect(el.isActive).toBe(false);
|
|
117
|
+
kitRouter.pushState({ url: "/docs/intro" });
|
|
118
|
+
expect(el.isActive).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("honours match-nested", async () => {
|
|
122
|
+
const nested = fixture<TestElement>(
|
|
123
|
+
`<${TAG} route-href="/docs" match-nested></${TAG}>`
|
|
124
|
+
);
|
|
125
|
+
const exact = fixture<TestElement>(`<${TAG} route-href="/docs"></${TAG}>`);
|
|
126
|
+
await wait(0);
|
|
127
|
+
expect(nested.routeInstance!.opts).toEqual({ matchNested: true });
|
|
128
|
+
expect(exact.routeInstance!.opts).toEqual({ matchNested: false });
|
|
129
|
+
kitRouter.pushState({ url: "/docs/intro" });
|
|
130
|
+
expect(nested.isActive).toBe(true);
|
|
131
|
+
expect(exact.isActive).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("swaps the router registration when route-href changes", async () => {
|
|
135
|
+
const el = fixture<TestElement>(`<${TAG} route-href="/a"></${TAG}>`);
|
|
136
|
+
await wait(0);
|
|
137
|
+
const first = el.routeInstance!;
|
|
138
|
+
expect(first.key).toBe("/a");
|
|
139
|
+
|
|
140
|
+
el.routeHref = "/b";
|
|
141
|
+
await wait(0);
|
|
142
|
+
const second = el.routeInstance!;
|
|
143
|
+
expect(second).not.toBe(first);
|
|
144
|
+
expect(second.key).toBe("/b");
|
|
145
|
+
expect(offSpy).toHaveBeenCalledWith(first);
|
|
146
|
+
expect(onSpy).toHaveBeenCalledWith(second);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("switches from a path route to a regex route", async () => {
|
|
150
|
+
const el = fixture<TestElement>(`<${TAG} route-href="/a"></${TAG}>`);
|
|
151
|
+
await wait(0);
|
|
152
|
+
const pathRoute = el.routeInstance!;
|
|
153
|
+
|
|
154
|
+
el.routeHref = null;
|
|
155
|
+
el.routeRegex = "^/z";
|
|
156
|
+
await wait(0);
|
|
157
|
+
expect(offSpy).toHaveBeenCalledWith(pathRoute);
|
|
158
|
+
expect(el.routeInstance!.key).toBeInstanceOf(RegExp);
|
|
159
|
+
expect((el.routeInstance!.key as RegExp).source).toBe(
|
|
160
|
+
new RegExp("^/z").source
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("unregisters when route-href is cleared", async () => {
|
|
165
|
+
const el = fixture<TestElement>(`<${TAG} route-href="/a"></${TAG}>`);
|
|
166
|
+
await wait(0);
|
|
167
|
+
const route = el.routeInstance!;
|
|
168
|
+
onSpy.mockClear();
|
|
169
|
+
|
|
170
|
+
el.routeHref = null;
|
|
171
|
+
await wait(0);
|
|
172
|
+
expect(el.routeInstance).toBeNull();
|
|
173
|
+
expect(offSpy).toHaveBeenCalledWith(route);
|
|
174
|
+
expect(onSpy).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("unregisters on disconnect and re-registers on reconnect", async () => {
|
|
178
|
+
const el = fixture<TestElement>(`<${TAG} route-href="/a"></${TAG}>`);
|
|
179
|
+
await wait(0);
|
|
180
|
+
const first = el.routeInstance!;
|
|
181
|
+
|
|
182
|
+
el.remove();
|
|
183
|
+
await wait(0);
|
|
184
|
+
expect(el.routeInstance).toBeNull();
|
|
185
|
+
expect(offSpy).toHaveBeenCalledWith(first);
|
|
186
|
+
|
|
187
|
+
onSpy.mockClear();
|
|
188
|
+
document.body.appendChild(el);
|
|
189
|
+
await wait(0);
|
|
190
|
+
expect(el.wasMounted).toBe(true);
|
|
191
|
+
const second = el.routeInstance!;
|
|
192
|
+
expect(second).toBeInstanceOf(KitRoute);
|
|
193
|
+
expect(second).not.toBe(first);
|
|
194
|
+
expect(onSpy).toHaveBeenCalledWith(second);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("re-registers a regex route on reconnect", async () => {
|
|
198
|
+
const el = fixture<TestElement>(`<${TAG} route-regex="^/r"></${TAG}>`);
|
|
199
|
+
await wait(0);
|
|
200
|
+
const first = el.routeInstance!;
|
|
201
|
+
expect(first).toBeInstanceOf(KitRoute);
|
|
202
|
+
|
|
203
|
+
el.remove();
|
|
204
|
+
await wait(0);
|
|
205
|
+
onSpy.mockClear();
|
|
206
|
+
document.body.appendChild(el);
|
|
207
|
+
await wait(0);
|
|
208
|
+
const second = el.routeInstance!;
|
|
209
|
+
expect(second).not.toBe(first);
|
|
210
|
+
expect(second.key).toBeInstanceOf(RegExp);
|
|
211
|
+
expect(onSpy).toHaveBeenCalledWith(second);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("does nothing on reconnect when no pattern is set", async () => {
|
|
215
|
+
const el = fixture<TestElement>(`<${TAG}></${TAG}>`);
|
|
216
|
+
await wait(0);
|
|
217
|
+
el.remove();
|
|
218
|
+
await wait(0);
|
|
219
|
+
document.body.appendChild(el);
|
|
220
|
+
await wait(0);
|
|
221
|
+
expect(el.routeInstance ?? null).toBeNull();
|
|
222
|
+
expect(onSpy).not.toHaveBeenCalled();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("keeps the registration when the element is moved synchronously", async () => {
|
|
226
|
+
const el = fixture<TestElement>(`<${TAG} route-href="/a"></${TAG}>`);
|
|
227
|
+
await wait(0);
|
|
228
|
+
const route = el.routeInstance!;
|
|
229
|
+
offSpy.mockClear();
|
|
230
|
+
onSpy.mockClear();
|
|
231
|
+
|
|
232
|
+
const holder = document.createElement("div");
|
|
233
|
+
document.body.appendChild(holder);
|
|
234
|
+
// remove + append in the same task is a "move" (isMoving), not a
|
|
235
|
+
// disconnect: the route must stay registered exactly once
|
|
236
|
+
el.remove();
|
|
237
|
+
holder.appendChild(el);
|
|
238
|
+
await wait(0);
|
|
239
|
+
expect(el.routeInstance).toBe(route);
|
|
240
|
+
expect(offSpy).not.toHaveBeenCalled();
|
|
241
|
+
expect(onSpy).not.toHaveBeenCalled();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { RoutableElement } from "../../index";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
fixture,
|
|
7
|
+
it,
|
|
8
|
+
vi,
|
|
9
|
+
wait,
|
|
10
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
11
|
+
|
|
12
|
+
const TAG = "routable-element-test";
|
|
13
|
+
if (!customElements.get(TAG)) {
|
|
14
|
+
RoutableElement.define(TAG);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("RoutableElement", () => {
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
document.body.innerHTML = "";
|
|
20
|
+
vi.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("throws when routeChanged is not implemented", () => {
|
|
24
|
+
const el = fixture<any>(`<${TAG}></${TAG}>`);
|
|
25
|
+
expect(() => el.routeChanged()).toThrowError(/routeChanged/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("has default props", () => {
|
|
29
|
+
const el = fixture<any>(`<${TAG}></${TAG}>`);
|
|
30
|
+
expect(el).dom.to.equalTag(`<${TAG}></${TAG}>`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("sets routeInstance to null on disconnect when routeHref is set", async () => {
|
|
34
|
+
const el = fixture<any>(`<${TAG}></${TAG}>`);
|
|
35
|
+
el.routeChanged = vi.fn();
|
|
36
|
+
el.routeHref = "/test";
|
|
37
|
+
await wait(0);
|
|
38
|
+
el.remove();
|
|
39
|
+
await wait(0);
|
|
40
|
+
expect(el.routeInstance).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
});
|