@excom/spa-route 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/spa-route.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/spa-route.build_docs.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/spa-route.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 +5 -0
- package/index.css +5 -0
- package/index.ts +29 -0
- package/package.json +48 -0
- package/rush-logs/spa-route.apply-exports.cache.log +1 -0
- package/rush-logs/spa-route.apply-exports.log +1 -0
- package/rush-logs/spa-route.build_docs.cache.log +1 -0
- package/rush-logs/spa-route.build_docs.log +1 -0
- package/rush-logs/spa-route.build_package-metas.cache.log +1 -0
- package/rush-logs/spa-route.build_package-metas.log +1 -0
- package/spa-a.ts +124 -0
- package/spa-manager.ts +605 -0
- package/spa-route.ts +322 -0
- package/src/spa-route.css +24 -0
- package/src/utils.ts +31 -0
- package/support/custom-elements.json +953 -0
- package/support/dist-docs/spa-a.md +40 -0
- package/support/dist-docs/spa-manager.md +66 -0
- package/support/dist-docs/spa-route.md +408 -0
- package/support/docs/README.md +314 -0
- package/support/package-meta.json +789 -0
- package/support/tests/spa-navigation.test.ts +1076 -0
- package/support/tests/spa-route.test.ts +766 -0
- package/support/tests/spa-title.test.ts +225 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `<spa-a>`
|
|
2
|
+
|
|
3
|
+
> Declarative SPA link with active-state styling.
|
|
4
|
+
|
|
5
|
+
**Tag:** `<spa-a>`
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
### Attributes
|
|
10
|
+
|
|
11
|
+
| Name | Surface | Type | Default | Values | Description | Inherited from |
|
|
12
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
13
|
+
| `route-action` | option | `string` | `"push"` | `"push"` \| `"replace"` \| `"back"` \| `"forward"` | Navigation mode when activated (default click). | |
|
|
14
|
+
| `document-title` | option | `string` | | | Sets `document.title` after navigation. | |
|
|
15
|
+
| `transition-types` | option | `tokenlist` | | `<token>…` | Space-separated View Transition types for this navigation (CSS View Transitions API). Useful for shared-element morphs like card expansion. | |
|
|
16
|
+
| `match-hash` | option | `boolean` | | | Require the URL hash to match when setting `is-active`. | |
|
|
17
|
+
| `is-active` | state | `boolean` | | | Current URL matches `route-href`. Style with `spa-a[is-active]`. | |
|
|
18
|
+
| `was-active` | state | `boolean` | | | Previous URL matched `route-href`. Style outgoing links / card-expansion exits with `spa-a[was-active]`. | |
|
|
19
|
+
| `host-ref` | option | `string` | | `<CSS Selector>` \| `"window"` \| `"document"` \| `"html"` \| `"body"` \| `"head"` | Listen on another element / `window` / `document` — e.g. Escape to dismiss a dialog from a global `keydown`. Defaults to `:scope`. Used with `listen-for`. Not compatible with `listen-for-lifecycle`. The selector MUST resolve when `host-ref` is set — it will not wait for a match to appear. | `@excom/listenable-element` |
|
|
20
|
+
| `listen-for` | option | `tokenlist` | | `<EventName>…` | Space-separated event names to listen for. Defaults to `click` when unset (and no lifecycle list is set). | `@excom/listenable-element` |
|
|
21
|
+
| `listen-for-lifecycle` | option | `tokenlist` | | `"connected"` \| `"disconnected"` \| `"adopted"` | Space-separated element lifecycles to handle. | `@excom/listenable-element` |
|
|
22
|
+
| `listen-once` | option | `boolean` | | | Handle each distinct event name / lifecycle at most once. | `@excom/listenable-element` |
|
|
23
|
+
| `selector-filter` | option | `string` | | `<CSS Selector>` | Only handle events whose `event.target` matches this CSS selector. Does not support `:scope` in the selector. | `@excom/listenable-element` |
|
|
24
|
+
| `keycode-filter` | option | `tokenlist` | | `<key` \| `mod+key>…` | Space-separated key filters (OR). Join modifiers with `+` (AND, any order): `shift+k tab` → Shift+K or Tab. Modifiers: `shift`, `alt`, `ctrl`/`control`, `meta`. Case-insensitive. | `@excom/listenable-element` |
|
|
25
|
+
| `pathname-filter` | option | `tokenlist` | | `<pathname>…` | Only handle when `location.pathname` is one of these values — route-aware behaviors without a separate router element. | `@excom/listenable-element` |
|
|
26
|
+
| `prevent-default` | option | `boolean` | | | Call `preventDefault()` on matched events (ignored for lifecycles). | `@excom/listenable-element` |
|
|
27
|
+
| `stop-propagation` | option | `boolean` | | | Call `stopPropagation()` on matched events (ignored for lifecycles). | `@excom/listenable-element` |
|
|
28
|
+
| `stop-immediate-propagation` | option | `boolean` | | | Call `stopImmediatePropagation()` on matched events (ignored for lifecycles). | `@excom/listenable-element` |
|
|
29
|
+
| `vibrate-ms` | option | `number` | `"20 (when attribute is present with no value)"` | | Vibrate on handle (`navigator.vibrate`). Empty / `0` uses a 20ms pulse. | `@excom/listenable-element` |
|
|
30
|
+
| `delay-ms` | option | `number` | | | Delay handling by this many milliseconds. | `@excom/listenable-element` |
|
|
31
|
+
| `is-debounced` | option | `boolean` | | | With `delay-ms`, coalesce bursts into one trailing call (debounce). | `@excom/listenable-element` |
|
|
32
|
+
| `route-href` | option | `string` | | `<path pattern>` | URL pattern to match. Supports named placeholders (`/users/:id`) and wildcards. Set this or `route-regex`. | `@excom/routable-element` |
|
|
33
|
+
| `route-regex` | option | `string` | | `<RegExp source>` | RegExp source string matched against the current URL — alternative to `route-href` for catch-alls / advanced patterns. | `@excom/routable-element` |
|
|
34
|
+
| `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. | `@excom/routable-element` |
|
|
35
|
+
|
|
36
|
+
### CSS Aliases
|
|
37
|
+
|
|
38
|
+
| Alias | Kind | Matches | Description |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| `:--spa-a` | element | `spa-a`, `.tag-spa-a` | |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# `<spa-manager>`
|
|
2
|
+
|
|
3
|
+
> SPA shell — view transitions, swipe nav, route orchestration.
|
|
4
|
+
|
|
5
|
+
**Tag:** `<spa-manager>`
|
|
6
|
+
|
|
7
|
+
## API
|
|
8
|
+
|
|
9
|
+
### Attributes
|
|
10
|
+
|
|
11
|
+
| Name | Surface | Type | Default | Values | Description | Inherited from |
|
|
12
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
13
|
+
| `transition-first-render` | option | `boolean` | | | Animate the very first activation (cold load) with a View Transition. Off by default so the initial paint is instant. | |
|
|
14
|
+
| `transition-delay` | option | `number` | | | Delay (ms) before starting the batched View Transition. Gives late sibling render/unrender events time to queue, or room for last-second DOM work. Unset / `null` starts synchronously. | |
|
|
15
|
+
| `no-transition` | option | `boolean` | | | Disable View Transitions for every child route. | |
|
|
16
|
+
| `render-timeout` | option | `number` | `2000` | | Max wait (ms) for child render promises before forcing the transition to resolve. Raise for slow remote templates. | |
|
|
17
|
+
| `overscroll-behavior-x` | option | `string` | | `"none"` \| `"navigate"` | Touch edge-swipe on touch devices. `none` blocks horizontal overscroll; `navigate` also calls back / forward past the threshold. | |
|
|
18
|
+
| `overscroll-x-threshold` | option | `number` | `40` | | Edge inset (px) where a touch start counts as an edge swipe. | |
|
|
19
|
+
| `max-states` | option | `number` | | | Cap retained router history states (scroll positions, transition types, etc.). | |
|
|
20
|
+
| `last-move` | state | `string` | | `"push"` \| `"replace"` \| `"back"` \| `"forward"` | Last navigation direction (`push` / `replace` / `back` / `forward`). Pick CSS transition styles from this. | |
|
|
21
|
+
| `active-url` | state | `string` | | | URL of the currently active route. | |
|
|
22
|
+
| `is-transitioning` | state | `boolean` | | | A View Transition is in flight. | |
|
|
23
|
+
| `has-rendered` | state | `boolean` | | | At least one render has committed. Gates `transition-first-render`; useful for hiding loading shells. | |
|
|
24
|
+
| `route-href` | option | `string` | | `<path pattern>` | URL pattern to match. Supports named placeholders (`/users/:id`) and wildcards. Set this or `route-regex`. | `@excom/routable-element` |
|
|
25
|
+
| `route-regex` | option | `string` | | `<RegExp source>` | RegExp source string matched against the current URL — alternative to `route-href` for catch-alls / advanced patterns. | `@excom/routable-element` |
|
|
26
|
+
| `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. | `@excom/routable-element` |
|
|
27
|
+
|
|
28
|
+
### Provision
|
|
29
|
+
|
|
30
|
+
| Name | Type | Description |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| `provision` | `KitRouteData` (`{ previous: { id: string; url: string; title?: string; isInit?: boolean; scrollX?: number; scrollY?: number; ttypes?: string[]; } \| null; active: { id: string; url: string; title?: string; isInit?: boolean; scrollX?: number; scrollY?: number; ttypes?: string[]; }; next: { id: string; url: string; title?: string; isInit?: boolean; scrollX?: number; scrollY?: number; ttypes?: string[]; } \| null; all: { id: string; url: string; title?: string; isInit?: boolean; scrollX?: number; scrollY?: number; ttypes?: string[]; }[]; params: Record<string, string> \| null; match: Array<any> \| null; move: null \| "push" \| "replace" \| "back" \| "forward"; event?: { hasUAVisualTransition: boolean; }; route: KitRoute; }`) | Current route payload. Not reflected as an attribute. |
|
|
33
|
+
|
|
34
|
+
### Recognized Elements
|
|
35
|
+
|
|
36
|
+
| Relationship | Selector | Required | Description |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| `spa-route` | descendant | yes | Routes this manager coordinates. Their render lifecycle is intercepted and batched into one view transition per navigation. |
|
|
39
|
+
|
|
40
|
+
### Fires
|
|
41
|
+
|
|
42
|
+
| Name | Type | Description |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| `spa-manager-will-transition` | `SpaManagerWillTransitionEvent` (`CustomEvent & { type: "spa-manager-will-transition"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Once per navigation, just before the View Transition starts. `preventDefault()` skips the transition for this navigation. |
|
|
45
|
+
| `spa-manager-transition` | `SpaManagerTransitionEvent` (`CustomEvent & { type: "spa-manager-transition"; detail: { transition: ViewTransition; }; bubbles: true; cancelable: true; composed: true }`) | After `document.startViewTransition()` is called. |
|
|
46
|
+
| `spa-manager-rendered` | `SpaManagerRenderedEvent` (`CustomEvent & { type: "spa-manager-rendered"; detail: void; bubbles: true; cancelable: true; composed: true }`) | After the transition (or synchronous fallback) finishes and routes are settled. |
|
|
47
|
+
| `spa-manager-error` | `SpaManagerErrorEvent` (`CustomEvent & { type: "spa-manager-error"; detail: unknown; bubbles: true; cancelable: true; composed: true }`) | Re-emitted when a child fires `spa-route-error`. `event.detail` mirrors the source error. |
|
|
48
|
+
| `spa-manager-push` | `SpaManagerPushEvent` (`CustomEvent & { type: "spa-manager-push"; detail: void; bubbles: true; cancelable: true; composed: true }`) | On `pushState` navigations. |
|
|
49
|
+
| `spa-manager-replace` | `SpaManagerReplaceEvent` (`CustomEvent & { type: "spa-manager-replace"; detail: void; bubbles: true; cancelable: true; composed: true }`) | On `replaceState` navigations. |
|
|
50
|
+
| `spa-manager-back` | `SpaManagerBackEvent` (`CustomEvent & { type: "spa-manager-back"; detail: void; bubbles: true; cancelable: true; composed: true }`) | On `back` navigations. |
|
|
51
|
+
| `spa-manager-forward` | `SpaManagerForwardEvent` (`CustomEvent & { type: "spa-manager-forward"; detail: void; bubbles: true; cancelable: true; composed: true }`) | On `forward` navigations. |
|
|
52
|
+
|
|
53
|
+
### Listens for
|
|
54
|
+
|
|
55
|
+
| Name | Type | Description |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `spa-route-render` | `RenderableRenderEvent` (`CustomEvent & { type: "{tag}-render"; detail: () => Promise<void>; bubbles: true; cancelable: true; composed: true }`) | Queues the child's render callback for the next batched transition. |
|
|
58
|
+
| `spa-route-unrender` | `RenderableUnrenderEvent` (`CustomEvent & { type: "{tag}-unrender"; detail: () => void; bubbles: true; cancelable: true; composed: true }`) | Queues the child's unrender callback (runs before renders so the outgoing route leaves first). |
|
|
59
|
+
| `spa-route-provision` | `SpaRouteProvisionEvent` (`CustomEvent & { type: "spa-route-provision"; detail: () => void; bubbles: true; cancelable: true; composed: true }`) | Queues a same-route param update (reuse) into the next batched transition. |
|
|
60
|
+
| `spa-route-error` | `RenderableErrorEvent` (`CustomEvent & { type: "{tag}-error"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Falls back to a non-transitioned update and re-emits as `spa-manager-error`. |
|
|
61
|
+
|
|
62
|
+
### Default actions
|
|
63
|
+
|
|
64
|
+
| Event | Default behavior (unless preventDefault() is called) |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| `spa-manager-will-transition` | Starts the batched View Transition (or synchronous update when transitions are unavailable / opted out). |
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# spa-route
|
|
2
|
+
|
|
3
|
+
Build a full SPA from HTML alone — screens, links, and view transitions.
|
|
4
|
+
|
|
5
|
+
> This site is a live demo... inspect its HTML! Other live demos of spa-route coming soon.
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<spa-manager>
|
|
9
|
+
<spa-route route-href="/" template-ref="/views/home.html"></spa-route>
|
|
10
|
+
<spa-route route-href="/about" template-ref="/views/about.html"></spa-route>
|
|
11
|
+
</spa-manager>
|
|
12
|
+
|
|
13
|
+
<nav>
|
|
14
|
+
<spa-a route-href="/">Home</spa-a>
|
|
15
|
+
<spa-a route-href="/about">About</spa-a>
|
|
16
|
+
</nav>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Pure CSS View Transitions** Write CSS, get beautiful animations between routes
|
|
22
|
+
- **Active / was-active** Style current and outgoing links & screens (nav chrome, card expansion)
|
|
23
|
+
- **Same-route reuse / refresh** Keep or rebuild the view when only params change
|
|
24
|
+
- **Scroll reset / restore** Per-axis control for push, replace, back, forward
|
|
25
|
+
- **Nested layouts** Keep a parent route mounted under child paths
|
|
26
|
+
- **404 fallbacks** Catch-alls that only fire when nothing else matched
|
|
27
|
+
- **Per-route document title** `document.title` follows the active route
|
|
28
|
+
- **History actions** Push, replace, back, forward from a link
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
`@excom/spa-route` v0.1.0
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pnpm add @excom/spa-route
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @excom/spa-route
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
yarn add @excom/spa-route
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Import
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import "@excom/spa-route";
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Wrap screens in `<spa-manager>`, give each `<spa-route>` a `route-href`, and link with `<spa-a>`.
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<!-- Optional SPA manager for View Transitions and batched router config -->
|
|
61
|
+
<spa-manager>
|
|
62
|
+
<!-- SPA routing -->
|
|
63
|
+
<spa-route route-href="/" template-ref="/views/home.html"></spa-route>
|
|
64
|
+
<spa-route route-href="/about" template-ref="/views/about.html"></spa-route>
|
|
65
|
+
</spa-manager>
|
|
66
|
+
<nav>
|
|
67
|
+
<!-- SPA links -->
|
|
68
|
+
<spa-a route-href="/">Home</spa-a>
|
|
69
|
+
<spa-a route-href="/about">About</spa-a>
|
|
70
|
+
</nav>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### API Reference
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
#### Attributes
|
|
77
|
+
|
|
78
|
+
| Name | Surface | Type | Default | Values | Description | Inherited from |
|
|
79
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
80
|
+
| `same-route` | option | `string` | `"reuse"` | `"reuse"` \| `"refresh"` | When this route matches while already active, `reuse` keeps the rendered tree and updates route data; `refresh` tears down and re-renders. Use `refresh` for param-driven screens (e.g. `/users/:id` → `/users/2`); `reuse` when only route data should change (e.g. `/logs/:view`). Pair with `scroll-set-disabled` to leave the viewport untouched. | |
|
|
81
|
+
| `no-transition` | option | `boolean` | | | Opt this route out of the parent `<spa-manager>` View Transition. Still renders/unrenders — just without the cross-fade. | |
|
|
82
|
+
| `scroll-reset-behavior` | option | `string` | `"instant"` | `"auto"` \| `"instant"` \| `"smooth"` | `window.scrollTo` behavior when this route applies a scroll reset / restore. | |
|
|
83
|
+
| `scroll-reset-x` | option | `tokenlist` | `"push replace"` | `"push"` \| `"replace"` \| `"back"` \| `"forward"` | Navigation moves that reset scroll X to `0`. Moves omitted here restore the saved X for that history entry instead. | |
|
|
84
|
+
| `scroll-reset-y` | option | `tokenlist` | `"push replace"` | `"push"` \| `"replace"` \| `"back"` \| `"forward"` | Navigation moves that reset scroll Y to `0`. Moves omitted here restore the saved Y for that history entry instead. | |
|
|
85
|
+
| `scroll-set-disabled` | option | `boolean` | | | Disable all scroll reset / restore for this route. | |
|
|
86
|
+
| `is-fallback` | option | `boolean` | | | Only activate when this route matches *and* no earlier sibling `<spa-route>` is already active. Pair with a permissive `route-regex` (e.g. `.*`) for 404 catch-alls. | |
|
|
87
|
+
| `document-title` | option | `string` | | | `document.title` while this route is active. The outermost `<spa-manager>` applies the last active route carrying one — so a nested route beats its ancestor — and restores the page's own `<title>` once no active route has one. Cold loads and back / forward retitle too: it keys off activation, not clicks. | |
|
|
88
|
+
| `was-active` | state | `boolean` | | | Set briefly while navigating away. Style outgoing screens / card-expansion exits with `spa-route[was-active]`. | |
|
|
89
|
+
| `template-ref` | option | `string` | `":scope > template"` | `<CSS Selector>` \| `<URL>` | Source `<template>` — in-document selector or remote URL. Changing mid-flight aborts and reloads. Can use `:scope` to relatively select elements: e.g. `main:has(:scope) > template` | `@excom/renderable-element` |
|
|
90
|
+
| `bypass-cache` | option | `boolean` | | | Skip the in-memory response cache (URL `template-ref` only). | `@excom/renderable-element` |
|
|
91
|
+
| `pre-fetch` | option | `string` | `"lazy"` | `""` \| `"eager"` \| `"idle"` \| `"lazy"` | When to fetch the template, independent of when it renders. `""` aliases `eager`. | `@excom/renderable-element` |
|
|
92
|
+
| `persist-content` | option | `boolean` | | | Reuse the same live nodes across unrender / render (held on `_persistedTree`) so form values, scroll position, and subtree state survive toggles. | `@excom/renderable-element` |
|
|
93
|
+
| `host-ref` | option | `string` | | `"shadow"` \| `"iframe"` \| `<CSS Selector>` | Where rendered children land. Unset = this element's light DOM. `shadow` attaches an open shadow root. `iframe` paints into a child `<iframe data-render-host>` body (you supply the iframe — useful for sandboxed / third-party document isolation). Any other value is a portal selector. | `@excom/renderable-element` |
|
|
94
|
+
| `ready-on` | option | `string` | | `<Event Name>` | Event name that marks rendered children "ready". Until it fires, `delaying-ready` is set so CSS can hide the host for a coordinated paint / view transition. | `@excom/renderable-element` |
|
|
95
|
+
| `is-active` | hybrid | `boolean` | | | Master switch. Set to load (if needed) and render; unset to unrender. Drive from visibility, route match, hover, etc. | `@excom/renderable-element` |
|
|
96
|
+
| `is-loading` | state | `boolean` | | | Template fetch in flight. | `@excom/renderable-element` |
|
|
97
|
+
| `did-load` | state | `boolean` | | | Template resolved at least once. Stays set across `is-active` toggles so consumers know later paints are warm (URL refs reuse the shared fetch cache in kit-utils). Cleared when `template-ref` changes or `--reload` forces a fresh resolve. | `@excom/renderable-element` |
|
|
98
|
+
| `is-error` | state | `boolean` | | | Latest template fetch rejected (excluding abort). Fires with the `error` event. | `@excom/renderable-element` |
|
|
99
|
+
| `delaying-ready` | state | `boolean` | | | Between `render` and the matching `ready-on` event. Hook with CSS for coordinated paints / view transitions. | `@excom/renderable-element` |
|
|
100
|
+
| `route-href` | option | `string` | | `<path pattern>` | URL pattern to match. Supports named placeholders (`/users/:id`) and wildcards. Set this or `route-regex`. | `@excom/routable-element` |
|
|
101
|
+
| `route-regex` | option | `string` | | `<RegExp source>` | RegExp source string matched against the current URL — alternative to `route-href` for catch-alls / advanced patterns. | `@excom/routable-element` |
|
|
102
|
+
| `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. | `@excom/routable-element` |
|
|
103
|
+
|
|
104
|
+
#### Provision
|
|
105
|
+
|
|
106
|
+
| Name | Type | Description |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| `provision` | `SpaRouteProvision` (`{ routeHref: string; matchNested: boolean; scrollResetY: string[]; scrollResetX: string[]; scrollResetBehavior: ScrollBehavior; noTransition: boolean; active: { id: string; url: string; title?: string; isInit?: boolean; scrollX?: number; scrollY?: number; ttypes?: string[]; }; event: { hasUAVisualTransition: boolean; }; match: Array<any> \| null; move: null \| "push" \| "replace" \| "back" \| "forward"; next: { id: string; url: string; title?: string; isInit?: boolean; scrollX?: number; scrollY?: number; ttypes?: string[]; } \| null; previous: { id: string; url: string; title?: string; isInit?: boolean; scrollX?: number; scrollY?: number; ttypes?: string[]; } \| null; params: Record<string, string> \| null; }`) | Active route payload for this activation (`null` when inactive). Not reflected as an attribute. |
|
|
109
|
+
|
|
110
|
+
#### Recognized Elements
|
|
111
|
+
|
|
112
|
+
| Relationship | Selector | Required | Description | Inherited from |
|
|
113
|
+
| --- | --- | --- | --- | --- |
|
|
114
|
+
| `template` | child | yes | Screen content. Cloned (or reused with `persist-content`) on activation. | |
|
|
115
|
+
| `iframe[data-render-host]` | child | no | Required when `host-ref="iframe"`. Content paints into `iframe.contentDocument.body`. Provide your own iframe (e.g. with `srcdoc`); the element will not create one. | `@excom/renderable-element` |
|
|
116
|
+
|
|
117
|
+
#### Fires
|
|
118
|
+
|
|
119
|
+
| Name | Type | Description | Inherited from |
|
|
120
|
+
| --- | --- | --- | --- |
|
|
121
|
+
| `spa-route-provision` | `SpaRouteProvisionEvent` (`CustomEvent & { type: "spa-route-provision"; detail: () => void; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Same route stayed active but params changed (`same-route="reuse"`). `event.detail` is a thunk that updates route data and resolves the ready promise. `<spa-manager>` batches this into the View Transition like render/unrender. | |
|
|
122
|
+
| `spa-route-render` | `RenderableRenderEvent` (`CustomEvent & { type: "{tag}-render"; detail: () => Promise<void>; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Dispatched when the element becomes active and is about to place template content into the host. `event.detail` is a thunk that performs the load (if not already loaded) and renders the children, returning a Promise that resolves once the corresponding `ready-on` event fires (or immediately if `ready-on` is unset). The promise rejects if the element is torn down mid-flight (`startTeardown` while loading / `delaying-ready`). Call `preventDefault()` to defer rendering and invoke `event.detail()` later. | `@excom/renderable-element` |
|
|
123
|
+
| `spa-route-unrender` | `RenderableUnrenderEvent` (`CustomEvent & { type: "{tag}-unrender"; detail: () => void; bubbles: true; cancelable: true; composed: true }`) | Cancelable. Dispatched when the element becomes inactive and content is already painted. `event.detail` is a thunk that removes the rendered children. Call `preventDefault()` to defer the removal. Not fired when teardown cancels an in-flight load — that path emits `aborted` instead. | `@excom/renderable-element` |
|
|
124
|
+
| `spa-route-did-render` | `RenderableDidRenderEvent` (`CustomEvent & { type: "{tag}-did-render"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched after the template content has actually been placed into the host. | `@excom/renderable-element` |
|
|
125
|
+
| `spa-route-did-unrender` | `RenderableDidUnrenderEvent` (`CustomEvent & { type: "{tag}-did-unrender"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched after rendered children have been removed from the host. | `@excom/renderable-element` |
|
|
126
|
+
| `spa-route-error` | `RenderableErrorEvent` (`CustomEvent & { type: "{tag}-error"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched when the template promise rejects with anything other than an `AbortError`. | `@excom/renderable-element` |
|
|
127
|
+
| `spa-route-aborted` | `RenderableAbortedEvent` (`CustomEvent & { type: "{tag}-aborted"; detail: void; bubbles: true; cancelable: true; composed: true }`) | Dispatched when an in-flight load / ready wait is canceled because `is-active` was unset (via `startTeardown`). | `@excom/renderable-element` |
|
|
128
|
+
|
|
129
|
+
#### Commands
|
|
130
|
+
|
|
131
|
+
| Command | Action | Inherited from |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `--reload` | Aborts any in-flight fetch and re-resolves the template, bypassing the cache for URL refs (useful after remote content changes). | `@excom/renderable-element` |
|
|
134
|
+
|
|
135
|
+
#### Default actions
|
|
136
|
+
|
|
137
|
+
| Event | Default behavior (unless preventDefault() is called) | Inherited from |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| `spa-route-provision` | Invokes `event.detail()` to apply the new provision. | |
|
|
140
|
+
| `spa-route-render` | Invokes `event.detail()` to load (if needed) and render the template into the host. | `@excom/renderable-element` |
|
|
141
|
+
| `spa-route-unrender` | Invokes `event.detail()` to remove rendered children from the host. | `@excom/renderable-element` |
|
|
142
|
+
|
|
143
|
+
#### CSS Aliases
|
|
144
|
+
|
|
145
|
+
| Alias | Kind | Matches | Description |
|
|
146
|
+
| --- | --- | --- | --- |
|
|
147
|
+
| `:--spa-route` | element | `spa-route`, `.tag-spa-route` | |
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
### Examples
|
|
152
|
+
|
|
153
|
+
#### Minimal SPA
|
|
154
|
+
|
|
155
|
+
Three routes, three links. Active links style via `spa-a[is-active]`.
|
|
156
|
+
|
|
157
|
+
```html
|
|
158
|
+
<spa-manager>
|
|
159
|
+
<nav>
|
|
160
|
+
<spa-a route-href="/home">Home</spa-a>
|
|
161
|
+
<spa-a route-href="/users">Users</spa-a>
|
|
162
|
+
<spa-a route-href="/about">About</spa-a>
|
|
163
|
+
<spa-a route-href="/contact">Contact</spa-a>
|
|
164
|
+
</nav>
|
|
165
|
+
<spa-route route-href="/home">
|
|
166
|
+
<template>
|
|
167
|
+
<h3>Welcome</h3>
|
|
168
|
+
<p>Mounted because the URL matched <code>/home</code>.</p>
|
|
169
|
+
</template>
|
|
170
|
+
</spa-route>
|
|
171
|
+
<spa-route route-href="/users">
|
|
172
|
+
<template>
|
|
173
|
+
<h3>Users</h3>
|
|
174
|
+
<ul>
|
|
175
|
+
<li>Adam</li>
|
|
176
|
+
<li>Linus</li>
|
|
177
|
+
<li>Grace</li>
|
|
178
|
+
</ul>
|
|
179
|
+
</template>
|
|
180
|
+
</spa-route>
|
|
181
|
+
<spa-route route-href="/about" template-ref="/this/view/is/remote.html"></spa-route>
|
|
182
|
+
<spa-route route-href="/contact" template-ref="#this-view-is-dom-selected"></spa-route>
|
|
183
|
+
</spa-manager>
|
|
184
|
+
|
|
185
|
+
<template id="this-view-is-dom-selected">foo@bar.com</template>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```css
|
|
189
|
+
spa-a[is-active] {
|
|
190
|
+
font-weight: bold;
|
|
191
|
+
pointer-events: none;
|
|
192
|
+
text-decoration: none;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Nested layout & 404
|
|
197
|
+
|
|
198
|
+
`match-nested` keeps a layout mounted under child paths. `is-fallback` with `route-regex=".*"` is a 404 that only activates when no preceding sibling matched.
|
|
199
|
+
|
|
200
|
+
```html
|
|
201
|
+
<spa-manager>
|
|
202
|
+
<nav>
|
|
203
|
+
<spa-a route-href="/users">Users list</spa-a>
|
|
204
|
+
<spa-a route-href="/users/42">User 42</spa-a>
|
|
205
|
+
<spa-a route-href="/missing">Missing page</spa-a>
|
|
206
|
+
</nav>
|
|
207
|
+
<spa-route route-href="/users" match-nested>
|
|
208
|
+
<template>
|
|
209
|
+
<section>
|
|
210
|
+
<h3>Users layout</h3>
|
|
211
|
+
<spa-manager>
|
|
212
|
+
<spa-route route-href="/users">
|
|
213
|
+
<template><p>List of users.</p></template>
|
|
214
|
+
</spa-route>
|
|
215
|
+
<spa-route route-href="/users/:id">
|
|
216
|
+
<template><p>Detail for a single user.</p></template>
|
|
217
|
+
</spa-route>
|
|
218
|
+
</spa-manager>
|
|
219
|
+
</section>
|
|
220
|
+
</template>
|
|
221
|
+
</spa-route>
|
|
222
|
+
<spa-route route-regex=".*?view=admin.*" template-ref="/views/admin-sidebar.html"></spa-route>
|
|
223
|
+
<spa-route route-regex=".*" is-fallback>
|
|
224
|
+
<template>
|
|
225
|
+
<section>
|
|
226
|
+
<h3>404</h3>
|
|
227
|
+
<p>Catch-all — only when no preceding sibling matched.</p>
|
|
228
|
+
</section>
|
|
229
|
+
</template>
|
|
230
|
+
</spa-route>
|
|
231
|
+
</spa-manager>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Document title
|
|
235
|
+
|
|
236
|
+
`document-title` sets `document.title` while its route is active. It keys off
|
|
237
|
+
activation, not clicks, so cold loads and back / forward retitle too. The
|
|
238
|
+
outermost `<spa-manager>` applies the last active route carrying one — a
|
|
239
|
+
nested route beats its ancestor — and restores the page's own `<title>` once
|
|
240
|
+
no active route has a title.
|
|
241
|
+
|
|
242
|
+
```html
|
|
243
|
+
<title>Nucleus · docs</title>
|
|
244
|
+
|
|
245
|
+
<spa-manager>
|
|
246
|
+
<spa-route route-href="/" document-title="My company">
|
|
247
|
+
<template><p>The company page.</p></template>
|
|
248
|
+
</spa-route>
|
|
249
|
+
<!-- untitled: the page's own <title> comes back -->
|
|
250
|
+
<spa-route route-href="/docs">
|
|
251
|
+
<template><p>The docs.</p></template>
|
|
252
|
+
</spa-route>
|
|
253
|
+
</spa-manager>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
#### History actions
|
|
257
|
+
|
|
258
|
+
`route-action="back"` / `"forward"` walk history; `"replace"` swaps the current entry instead of pushing.
|
|
259
|
+
|
|
260
|
+
```html
|
|
261
|
+
<spa-manager>
|
|
262
|
+
<nav>
|
|
263
|
+
<spa-a route-action="back">‹ Back</spa-a>
|
|
264
|
+
<spa-a route-action="forward">Forward ›</spa-a>
|
|
265
|
+
<spa-a route-href="/one">Push /one</spa-a>
|
|
266
|
+
<spa-a route-href="/two">Push /two</spa-a>
|
|
267
|
+
<spa-a route-href="/login" route-action="replace">
|
|
268
|
+
Replace with /login
|
|
269
|
+
</spa-a>
|
|
270
|
+
</nav>
|
|
271
|
+
<spa-route route-href="/one">
|
|
272
|
+
<template><p>You're on <code>/one</code>.</p></template>
|
|
273
|
+
</spa-route>
|
|
274
|
+
<spa-route route-href="/two">
|
|
275
|
+
<template><p>You're on <code>/two</code>.</p></template>
|
|
276
|
+
</spa-route>
|
|
277
|
+
<spa-route route-href="/login">
|
|
278
|
+
<template>
|
|
279
|
+
<p>You're on <code>/login</code> — this entry replaced the
|
|
280
|
+
previous one in history.</p>
|
|
281
|
+
</template>
|
|
282
|
+
</spa-route>
|
|
283
|
+
</spa-manager>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### View Transitions
|
|
287
|
+
|
|
288
|
+
`<spa-manager>` wraps each navigation in `document.startViewTransition()` (when supported). Style with `::view-transition-*`; set per-link types via `transition-types` (e.g. card expansion); opt a route out with `no-transition`.
|
|
289
|
+
|
|
290
|
+
```css
|
|
291
|
+
::view-transition-old(root),
|
|
292
|
+
::view-transition-new(root) {
|
|
293
|
+
animation-duration: 0.25s;
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### View Transitions - Localized
|
|
298
|
+
If you had a list of cards, and clicking on one expanded it to the detail view (and vice versa, contracting), you would achieve it similarly to the code example below. This technique relies on styling the `<spa-a>` with its `[is-active]` (incoming view) and `[was-active]` (outgoing view).
|
|
299
|
+
|
|
300
|
+
```html
|
|
301
|
+
<spa-manager>
|
|
302
|
+
<spa-route id="route-list" route-href="/list">
|
|
303
|
+
<template>
|
|
304
|
+
<spa-a route-href="/detail/123" transition-types="card-morph" class="mini-card">
|
|
305
|
+
Go to detail
|
|
306
|
+
</spa-a>
|
|
307
|
+
</template>
|
|
308
|
+
</spa-route>
|
|
309
|
+
<spa-route id="route-detail" route-href="/detail/:id">
|
|
310
|
+
<template>
|
|
311
|
+
<article id="detail-card" class="card">
|
|
312
|
+
<!-- other content here -->
|
|
313
|
+
</article>
|
|
314
|
+
</template>
|
|
315
|
+
</spa-route>
|
|
316
|
+
</spa-manager>
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
```css
|
|
320
|
+
html:active-view-transition-type(card-morph) {
|
|
321
|
+
#route-list spa-a[transition-types="card-morph"][was-active], /* outgoing list card (forward) */
|
|
322
|
+
#route-list spa-a[transition-types="card-morph"][is-active], /* incoming list card (back) */
|
|
323
|
+
#detail-card /* detail card (forward & back) */ {
|
|
324
|
+
contain: layout;
|
|
325
|
+
height: fit-content;
|
|
326
|
+
view-transition-name: card-morph;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
::view-transition-old(card-morph),
|
|
330
|
+
::view-transition-new(card-morph) {
|
|
331
|
+
mix-blend-mode: normal;
|
|
332
|
+
height: 100%;
|
|
333
|
+
width: 100%;
|
|
334
|
+
will-change: opacity;
|
|
335
|
+
animation-fill-mode: both;
|
|
336
|
+
}
|
|
337
|
+
::view-transition-old(card-morph) {
|
|
338
|
+
animation-name: fade-out 1s ease;
|
|
339
|
+
}
|
|
340
|
+
::view-transition-new(card-morph) {
|
|
341
|
+
animation-name: fade-in 1s ease;
|
|
342
|
+
}
|
|
343
|
+
@keyframes fade-in {
|
|
344
|
+
from { opacity: 0; }
|
|
345
|
+
to { opacity: 1; }
|
|
346
|
+
}
|
|
347
|
+
@keyframes fade-out {
|
|
348
|
+
from { opacity: 1; }
|
|
349
|
+
to { opacity: 0; }
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Touch edge-swipe
|
|
354
|
+
On touch devices, horizontal drags from within `overscroll-x-threshold` of an edge trigger back / forward. Use `"none"` to block overscroll without navigating. Useful for preventing native swipes in Safari, which visually break SPAs.
|
|
355
|
+
|
|
356
|
+
```html
|
|
357
|
+
<spa-manager overscroll-behavior-x="navigate"></spa-manager>
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
#### Scroll reset / restore
|
|
362
|
+
|
|
363
|
+
By default, `<spa-route>`:
|
|
364
|
+
- resets scroll to top-left on `push` / `replace`
|
|
365
|
+
- restores the saved scroll position on `back` / `forward`
|
|
366
|
+
|
|
367
|
+
Override per axis with `scroll-reset-y` / `scroll-reset-x` — space-separated moves that should reset to `0` (omitted moves restore instead):
|
|
368
|
+
|
|
369
|
+
```html
|
|
370
|
+
<!-- also reset Y when the user hits back -->
|
|
371
|
+
<spa-route
|
|
372
|
+
route-href="/article/:id"
|
|
373
|
+
scroll-reset-y="push replace back"
|
|
374
|
+
></spa-route>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Animate with `scroll-reset-behavior="smooth"`. Disable all scroll handling with `scroll-set-disabled`.
|
|
378
|
+
|
|
379
|
+
#### Same-route params
|
|
380
|
+
|
|
381
|
+
When the matched route stays the same but params change (e.g. `/users/1` → `/users/2`):
|
|
382
|
+
|
|
383
|
+
- `same-route="reuse"` (default) — keep the rendered tree, update route data, and let `<spa-manager>` run a View Transition
|
|
384
|
+
- `same-route="refresh"` — tear down and re-render the view
|
|
385
|
+
|
|
386
|
+
```html
|
|
387
|
+
<spa-route route-href="/users/:id" same-route="refresh">
|
|
388
|
+
<template><!-- fresh tree per user id --></template>
|
|
389
|
+
</spa-route>
|
|
390
|
+
|
|
391
|
+
<spa-route
|
|
392
|
+
route-href="/logs/:view"
|
|
393
|
+
same-route="reuse"
|
|
394
|
+
scroll-set-disabled
|
|
395
|
+
>
|
|
396
|
+
<template><!-- preserve content + scroll across view tabs --></template>
|
|
397
|
+
</spa-route>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
#### Transition delay
|
|
401
|
+
|
|
402
|
+
`transition-delay` on `<spa-manager>` waits N ms before starting the batched View Transition — useful when sibling routes need a beat to queue their render/unrender callbacks, or for last-second DOM work.
|
|
403
|
+
|
|
404
|
+
```html
|
|
405
|
+
<spa-manager transition-delay="50">
|
|
406
|
+
<!-- routes -->
|
|
407
|
+
</spa-manager>
|
|
408
|
+
```
|