@microsoft/webui-framework 0.0.9 β 0.0.11
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/README.md +13 -13
- package/dist/decorators.d.ts +29 -1
- package/dist/decorators.js +44 -38
- package/dist/element/diff.js +1 -1
- package/dist/element.d.ts +15 -7
- package/dist/element.js +83 -27
- package/dist/template.d.ts +6 -4
- package/dist/template.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,11 +8,11 @@ This package is the browser-side runtime used by `webui build --plugin=webui`. I
|
|
|
8
8
|
- `@observable`, `@attr`, and `@volatile` decorators
|
|
9
9
|
- compiled template path mapping for direct DOM binding resolution
|
|
10
10
|
- light DOM or shadow DOM rendering (`--dom=light|shadow` flag)
|
|
11
|
-
- SSR state seeding from `window.
|
|
11
|
+
- SSR state seeding from `window.__webui.state` (like Preact's props)
|
|
12
12
|
|
|
13
13
|
If you are building WebUI apps in this repo, this is the component model used by examples like `examples/app/todo-webui`, `examples/app/commerce`, and `examples/app/contact-book-manager`.
|
|
14
14
|
|
|
15
|
-
> π **Full documentation at [microsoft.github.io/webui](https://microsoft.github.io/webui)
|
|
15
|
+
> π **Full documentation at [microsoft.github.io/webui](https://microsoft.github.io/webui)**, see the [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity) for component authoring patterns. For framework internals (hydration, path resolution, reactive update model), see [RENDERING.md](./RENDERING.md).
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -93,7 +93,7 @@ Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--
|
|
|
93
93
|
cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
The compiler/plugin generates the template metadata consumed by the runtime. In normal app code, you should not need to hand-author `window.
|
|
96
|
+
The compiler/plugin generates the template metadata consumed by the runtime. In normal app code, you should not need to hand-author `window.__webui.templates`.
|
|
97
97
|
|
|
98
98
|
### DOM strategy (`--dom`)
|
|
99
99
|
|
|
@@ -107,7 +107,7 @@ The `--dom` flag controls how the server renders component content:
|
|
|
107
107
|
The runtime auto-detects which mode was used at hydration time:
|
|
108
108
|
- If a `shadowRoot` already exists β shadow DOM SSR path
|
|
109
109
|
- If `childNodes` exist but no shadow root β light DOM SSR path
|
|
110
|
-
- If neither β client-created path (uses `meta.sd`
|
|
110
|
+
- If neither β client-created path (uses `meta.sd` to decide)
|
|
111
111
|
|
|
112
112
|
Light DOM is useful for simpler styling (CSS inheritance works naturally) and
|
|
113
113
|
better search-engine indexing. Shadow DOM provides style encapsulation.
|
|
@@ -125,7 +125,7 @@ Base class for framework components.
|
|
|
125
125
|
| `static define(tagName)` | Register the class as a custom element |
|
|
126
126
|
| `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
|
|
127
127
|
| `$update()` | Force a reactive update (normally called automatically) |
|
|
128
|
-
| `
|
|
128
|
+
| `setState(state)` | Populate `@observable` properties from router/server state |
|
|
129
129
|
| `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
|
|
130
130
|
|
|
131
131
|
In most components you do not call `$update()` directly. Property changes through `@observable` and `@attr` trigger updates for you.
|
|
@@ -300,7 +300,7 @@ When contributing to the runtime, avoid these patterns:
|
|
|
300
300
|
β β β (Rust/Go/C#/β¦) β β β
|
|
301
301
|
β HTML template β β β β SSR HTML (light or β
|
|
302
302
|
β + expressions ββββββΆβ TemplateMeta (JSON) ββββββΆβ shadow DOM) + β
|
|
303
|
-
β + @if / @for β β + state data β β
|
|
303
|
+
β + @if / @for β β + state data β β __webui.state JSON β
|
|
304
304
|
β β β β β β
|
|
305
305
|
β Outputs: β β Renders: β β Hydrates: β
|
|
306
306
|
β β’ TemplateMeta β β β’ Full HTML page β β β’ Path-based DOM β
|
|
@@ -329,7 +329,7 @@ flowchart LR
|
|
|
329
329
|
subgraph Serve ["Server (Any Language)"]
|
|
330
330
|
M --> R[Route Handler]
|
|
331
331
|
S[State Data] --> R
|
|
332
|
-
R --> HTML["Full SSR HTML<br/>(shadow or light DOM)<br/>+ TemplateMeta <script><br/>+
|
|
332
|
+
R --> HTML["Full SSR HTML<br/>(shadow or light DOM)<br/>+ TemplateMeta <script><br/>+ __webui.state <script>"]
|
|
333
333
|
end
|
|
334
334
|
|
|
335
335
|
subgraph Browser ["Browser"]
|
|
@@ -377,7 +377,7 @@ graph TD
|
|
|
377
377
|
### SSR Hydration Path
|
|
378
378
|
|
|
379
379
|
When the server renders a component, it emits HTML content (as a declarative
|
|
380
|
-
shadow root or as light DOM children) along with a `window.
|
|
380
|
+
shadow root or as light DOM children) along with a `window.__webui.state`
|
|
381
381
|
JSON payload. The browser parses this DOM before any JavaScript runs.
|
|
382
382
|
When the component's JS loads and `connectedCallback` fires, the framework
|
|
383
383
|
uses compiled template paths to resolve SSR DOM nodes without any marker
|
|
@@ -390,13 +390,13 @@ sequenceDiagram
|
|
|
390
390
|
participant CE as Custom Element
|
|
391
391
|
participant FW as Framework
|
|
392
392
|
|
|
393
|
-
Server->>Browser: HTML (shadow or light DOM)<br/>+
|
|
393
|
+
Server->>Browser: HTML (shadow or light DOM)<br/>+ __webui.state JSON
|
|
394
394
|
Browser->>Browser: Parse HTML β DOM exists
|
|
395
395
|
Browser->>CE: Custom element upgrade
|
|
396
396
|
CE->>CE: attributeChangedCallback (pre-existing attrs)
|
|
397
397
|
CE->>FW: connectedCallback() β $mount()
|
|
398
398
|
FW->>FW: SSR DOM detected (shadow root or children exist)
|
|
399
|
-
FW->>FW: $applySSRState() β seed observables from
|
|
399
|
+
FW->>FW: $applySSRState() β seed observables from __webui.state
|
|
400
400
|
FW->>FW: $hydrate() β template-parallel path resolution
|
|
401
401
|
FW->>FW: $resolveSSR() β match SSR nodes via ordinal traversal
|
|
402
402
|
FW->>FW: $wireEvents() + $wireRefs()
|
|
@@ -552,7 +552,7 @@ browser sees `42` in the DOM but the JavaScript property `this.count` is still
|
|
|
552
552
|
`0` (the class default). Without seeding, the first `$update()` would
|
|
553
553
|
overwrite the SSR content with the wrong value.
|
|
554
554
|
|
|
555
|
-
State seeding uses `window.
|
|
555
|
+
State seeding uses `window.__webui.state` β a JSON object emitted by the
|
|
556
556
|
server handler as a `<script>` tag. Like Preact's props, this delivers the
|
|
557
557
|
same data used for SSR rendering to the client. During `$mount()`,
|
|
558
558
|
`$applySSRState()` writes matching keys directly to observable backing fields
|
|
@@ -560,7 +560,7 @@ before any bindings are wired:
|
|
|
560
560
|
|
|
561
561
|
```mermaid
|
|
562
562
|
flowchart LR
|
|
563
|
-
SCRIPT["<script><br/>window.
|
|
563
|
+
SCRIPT["<script><br/>window.__webui.state = {<br/> count: 42,<br/> title: 'Hello'<br/>}"] --> APPLY["$applySSRState()"]
|
|
564
564
|
APPLY --> SEED["Write to backing fields:<br/>this._count = 42<br/>this._title = 'Hello'"]
|
|
565
565
|
SEED --> HYDRATE["$hydrate() β bindings match<br/>server-rendered DOM"]
|
|
566
566
|
```
|
|
@@ -611,7 +611,7 @@ are removed; new items are appended.
|
|
|
611
611
|
On initial hydration, the repeat system walks existing SSR children and
|
|
612
612
|
reconstructs collection instances by matching them against the compiled
|
|
613
613
|
template via `$resolveSSR` path traversal. State is already seeded from
|
|
614
|
-
`window.
|
|
614
|
+
`window.__webui.state`, so repeat items reflect the server-rendered list
|
|
615
615
|
without parsing marker comments.
|
|
616
616
|
|
|
617
617
|
---
|
package/dist/decorators.d.ts
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Convert a camelCase DOM property name into its kebab-case HTML attribute form.
|
|
3
|
+
*
|
|
4
|
+
* This function is optimized for framework-level hot paths where attribute
|
|
5
|
+
* normalization may run thousands of times per render. It performs three
|
|
6
|
+
* progressively cheaper checks:
|
|
7
|
+
*
|
|
8
|
+
* 1. **Direct lookup for irregular mappings**
|
|
9
|
+
* Many DOM properties (e.g., `readOnly`, `tabIndex`, `crossOrigin`) do not
|
|
10
|
+
* follow simple camelCase β kebab-case rules. These are resolved through a
|
|
11
|
+
* precomputed `propertyToAttribute` map for O(1) returns with no string
|
|
12
|
+
* processing.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Fast path for ARIA attributes**
|
|
15
|
+
* ARIA properties always begin with `aria` followed by an uppercase letter
|
|
16
|
+
* (e.g., `ariaDescribedBy`). These map to `aria-` + the lowercase remainder.
|
|
17
|
+
* This branch avoids the general loop and uses the engine-optimized
|
|
18
|
+
* `.toLowerCase()` for the suffix.
|
|
19
|
+
*
|
|
20
|
+
* 3. **General camelCase β kebab-case conversion**
|
|
21
|
+
* For all other inputs, the function performs a tight ASCII-only scan:
|
|
22
|
+
* uppercase AβZ (65β90) are converted to lowercase and prefixed with `-`,
|
|
23
|
+
* while all other characters are copied as-is. This avoids regex engines,
|
|
24
|
+
* callback allocations, and match objects, producing predictable,
|
|
25
|
+
* allocation-minimal performance ideal for DOM attribute reflection.
|
|
26
|
+
*
|
|
27
|
+
* The result is a predictable, JIT-friendly transformation suitable for
|
|
28
|
+
* attribute diffing, SSR serialization, and runtime DOM patching.
|
|
29
|
+
*/
|
|
2
30
|
export declare function toKebabCase(str: string): string;
|
|
3
31
|
export declare function getObservableNames(ctor: Function): Set<string>;
|
|
4
32
|
/**
|
package/dist/decorators.js
CHANGED
|
@@ -12,44 +12,11 @@
|
|
|
12
12
|
/**
|
|
13
13
|
* Map of camelCase property names to their HTML attribute names.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* (e.g., `ariaDescribedBy` β `aria-describedby`), per the ARIAMixin spec.
|
|
19
|
-
* 2. HTML global/element attributes β concatenated lowercase attribute names
|
|
20
|
-
* with camelCase property counterparts (e.g., `readOnly` β `readonly`).
|
|
15
|
+
* ARIA attributes (`ariaXxxYyy β aria-` + lowercase remainder) are handled
|
|
16
|
+
* algorithmically in `toKebabCase`. Only HTML global/element attributes
|
|
17
|
+
* with irregular mappings (concatenated lowercase) need explicit entries.
|
|
21
18
|
*/
|
|
22
19
|
const propertyToAttribute = Object.assign(Object.create(null), {
|
|
23
|
-
// --- ARIA (ARIAMixin) ---
|
|
24
|
-
ariaActiveDescendant: 'aria-activedescendant',
|
|
25
|
-
ariaAutoComplete: 'aria-autocomplete',
|
|
26
|
-
ariaBrailleLabel: 'aria-braillelabel',
|
|
27
|
-
ariaBrailleRoleDescription: 'aria-brailleroledescription',
|
|
28
|
-
ariaColCount: 'aria-colcount',
|
|
29
|
-
ariaColIndex: 'aria-colindex',
|
|
30
|
-
ariaColIndexText: 'aria-colindextext',
|
|
31
|
-
ariaColSpan: 'aria-colspan',
|
|
32
|
-
ariaDescribedBy: 'aria-describedby',
|
|
33
|
-
ariaDropEffect: 'aria-dropeffect',
|
|
34
|
-
ariaErrorMessage: 'aria-errormessage',
|
|
35
|
-
ariaFlowTo: 'aria-flowto',
|
|
36
|
-
ariaHasPopup: 'aria-haspopup',
|
|
37
|
-
ariaKeyShortcuts: 'aria-keyshortcuts',
|
|
38
|
-
ariaLabelledBy: 'aria-labelledby',
|
|
39
|
-
ariaMultiLine: 'aria-multiline',
|
|
40
|
-
ariaMultiSelectable: 'aria-multiselectable',
|
|
41
|
-
ariaPosInSet: 'aria-posinset',
|
|
42
|
-
ariaReadOnly: 'aria-readonly',
|
|
43
|
-
ariaRoleDescription: 'aria-roledescription',
|
|
44
|
-
ariaRowCount: 'aria-rowcount',
|
|
45
|
-
ariaRowIndex: 'aria-rowindex',
|
|
46
|
-
ariaRowIndexText: 'aria-rowindextext',
|
|
47
|
-
ariaRowSpan: 'aria-rowspan',
|
|
48
|
-
ariaSetSize: 'aria-setsize',
|
|
49
|
-
ariaValueMax: 'aria-valuemax',
|
|
50
|
-
ariaValueMin: 'aria-valuemin',
|
|
51
|
-
ariaValueNow: 'aria-valuenow',
|
|
52
|
-
ariaValueText: 'aria-valuetext',
|
|
53
20
|
// --- HTML global/element attributes ---
|
|
54
21
|
accessKey: 'accesskey',
|
|
55
22
|
autoCapitalize: 'autocapitalize',
|
|
@@ -73,10 +40,49 @@ const propertyToAttribute = Object.assign(Object.create(null), {
|
|
|
73
40
|
tabIndex: 'tabindex',
|
|
74
41
|
useMap: 'usemap',
|
|
75
42
|
});
|
|
76
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Convert a camelCase DOM property name into its kebab-case HTML attribute form.
|
|
45
|
+
*
|
|
46
|
+
* This function is optimized for framework-level hot paths where attribute
|
|
47
|
+
* normalization may run thousands of times per render. It performs three
|
|
48
|
+
* progressively cheaper checks:
|
|
49
|
+
*
|
|
50
|
+
* 1. **Direct lookup for irregular mappings**
|
|
51
|
+
* Many DOM properties (e.g., `readOnly`, `tabIndex`, `crossOrigin`) do not
|
|
52
|
+
* follow simple camelCase β kebab-case rules. These are resolved through a
|
|
53
|
+
* precomputed `propertyToAttribute` map for O(1) returns with no string
|
|
54
|
+
* processing.
|
|
55
|
+
*
|
|
56
|
+
* 2. **Fast path for ARIA attributes**
|
|
57
|
+
* ARIA properties always begin with `aria` followed by an uppercase letter
|
|
58
|
+
* (e.g., `ariaDescribedBy`). These map to `aria-` + the lowercase remainder.
|
|
59
|
+
* This branch avoids the general loop and uses the engine-optimized
|
|
60
|
+
* `.toLowerCase()` for the suffix.
|
|
61
|
+
*
|
|
62
|
+
* 3. **General camelCase β kebab-case conversion**
|
|
63
|
+
* For all other inputs, the function performs a tight ASCII-only scan:
|
|
64
|
+
* uppercase AβZ (65β90) are converted to lowercase and prefixed with `-`,
|
|
65
|
+
* while all other characters are copied as-is. This avoids regex engines,
|
|
66
|
+
* callback allocations, and match objects, producing predictable,
|
|
67
|
+
* allocation-minimal performance ideal for DOM attribute reflection.
|
|
68
|
+
*
|
|
69
|
+
* The result is a predictable, JIT-friendly transformation suitable for
|
|
70
|
+
* attribute diffing, SSR serialization, and runtime DOM patching.
|
|
71
|
+
*/
|
|
77
72
|
export function toKebabCase(str) {
|
|
78
73
|
const mapped = propertyToAttribute[str];
|
|
79
|
-
|
|
74
|
+
if (mapped)
|
|
75
|
+
return mapped;
|
|
76
|
+
// ARIA properties: ariaXxxYyy β aria- + lowercase remainder
|
|
77
|
+
if (str.length > 4 && str.charCodeAt(0) === 97 /* a */ && str.startsWith('aria') && str.charCodeAt(4) >= 65 && str.charCodeAt(4) <= 90) {
|
|
78
|
+
return 'aria-' + str.slice(4).toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
let out = '';
|
|
81
|
+
for (let i = 0; i < str.length; i++) {
|
|
82
|
+
const code = str.charCodeAt(i);
|
|
83
|
+
out += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
80
86
|
}
|
|
81
87
|
/**
|
|
82
88
|
* Shared logic for installing a reactive getter/setter on a class prototype.
|
package/dist/element/diff.js
CHANGED
|
@@ -28,7 +28,7 @@ export function dotWalk(cursor, path, from) {
|
|
|
28
28
|
export function resolveRepeatValue(scopeVar, scope, path) {
|
|
29
29
|
if (path === scopeVar)
|
|
30
30
|
return scope;
|
|
31
|
-
if (!path.startsWith(
|
|
31
|
+
if (path.length <= scopeVar.length || path.charCodeAt(scopeVar.length) !== 46 /* '.' */ || !path.startsWith(scopeVar))
|
|
32
32
|
return undefined;
|
|
33
33
|
return dotWalk(scope, path, scopeVar.length + 1);
|
|
34
34
|
}
|
package/dist/element.d.ts
CHANGED
|
@@ -17,23 +17,31 @@ export declare class WebUIElement extends HTMLElement {
|
|
|
17
17
|
/** Mount the component after children are available. */
|
|
18
18
|
private $mount;
|
|
19
19
|
disconnectedCallback(): void;
|
|
20
|
+
/**
|
|
21
|
+
* Permanently destroy this component's own bindings and DOM references.
|
|
22
|
+
* Each component is responsible for its own cleanup β child WebUI
|
|
23
|
+
* elements handle theirs via their own `disconnectedCallback`.
|
|
24
|
+
*/
|
|
25
|
+
$destroy(): void;
|
|
26
|
+
/** Break all DOM references held by a binding instance and its nested blocks. */
|
|
27
|
+
private $teardown;
|
|
20
28
|
/** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
|
|
21
29
|
$emit(name: string, detail?: unknown): boolean;
|
|
22
|
-
/** Populate @observable properties from router state.
|
|
30
|
+
/** Populate @observable properties from server or router state.
|
|
23
31
|
*
|
|
24
32
|
* Each property is set through its reactive setter, which coalesces
|
|
25
33
|
* updates into a single pending microtask. We then synchronously
|
|
26
34
|
* flush those pending path updates so the DOM is current before any
|
|
27
35
|
* view-transition snapshot captures it.
|
|
28
36
|
*/
|
|
29
|
-
|
|
37
|
+
setState(state: Record<string, unknown>): void;
|
|
30
38
|
/**
|
|
31
|
-
* Apply SSR state from
|
|
39
|
+
* Apply SSR state from `window.__webui.state`.
|
|
32
40
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
41
|
+
* The handler emits all SSR metadata in a single consolidated
|
|
42
|
+
* `window.__webui` script block. State lives at `.state` β the same
|
|
43
|
+
* props passed to the server render so observables match the DOM.
|
|
44
|
+
* Only observable properties are set β unknown keys are ignored.
|
|
37
45
|
*
|
|
38
46
|
* Writes directly to the backing field (`_prop`) to avoid triggering
|
|
39
47
|
* reactive updates before bindings are wired.
|
package/dist/element.js
CHANGED
|
@@ -48,8 +48,10 @@ import { ATTR_KIND_BOOLEAN, ATTR_KIND_COMPLEX, ATTR_KIND_TEMPLATE, } from './ele
|
|
|
48
48
|
// ββ Caches ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
49
49
|
/** Parsed template cache β cloneNode(true) is faster than re-parsing. */
|
|
50
50
|
const templateCache = new WeakMap();
|
|
51
|
-
/** Parsed template DOM for SSR path mapping, keyed by
|
|
52
|
-
const templateDOMCache = new
|
|
51
|
+
/** Parsed template DOM for SSR path mapping, keyed by TemplateBlockMeta. */
|
|
52
|
+
const templateDOMCache = new WeakMap();
|
|
53
|
+
/** Cached root tag name extracted from meta.h before it's released. */
|
|
54
|
+
const rootTagCache = new WeakMap();
|
|
53
55
|
/** Pre-computed ordinals for template nodes: childIndex β [nodeType, ordinal].
|
|
54
56
|
* Avoids re-counting element/text siblings on every $resolveSSR call. */
|
|
55
57
|
const tplOrdinalCache = new WeakMap();
|
|
@@ -88,12 +90,12 @@ function childNodesArray(parent) {
|
|
|
88
90
|
}
|
|
89
91
|
// ββ Helper: parse template HTML into a temp container ββββββββββββ
|
|
90
92
|
function getTemplateDom(meta) {
|
|
91
|
-
let cached = templateDOMCache.get(meta
|
|
93
|
+
let cached = templateDOMCache.get(meta);
|
|
92
94
|
if (cached)
|
|
93
95
|
return cached;
|
|
94
96
|
const div = document.createElement('div');
|
|
95
97
|
div.innerHTML = meta.h;
|
|
96
|
-
templateDOMCache.set(meta
|
|
98
|
+
templateDOMCache.set(meta, div);
|
|
97
99
|
return div;
|
|
98
100
|
}
|
|
99
101
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -126,7 +128,7 @@ export class WebUIElement extends HTMLElement {
|
|
|
126
128
|
const meta = getTemplate(tag);
|
|
127
129
|
if (!meta) {
|
|
128
130
|
console.warn(`[WebUI] Template metadata for <${tag}> not found. ` +
|
|
129
|
-
`Ensure the component is included in the SSR output or registered via
|
|
131
|
+
`Ensure the component is included in the SSR output or registered via __webui.templates.`);
|
|
130
132
|
return;
|
|
131
133
|
}
|
|
132
134
|
this.$meta = meta;
|
|
@@ -152,7 +154,7 @@ export class WebUIElement extends HTMLElement {
|
|
|
152
154
|
hydrationStart();
|
|
153
155
|
// Auto-detect shadow vs light DOM
|
|
154
156
|
const hasShadow = !!this.shadowRoot;
|
|
155
|
-
const wantShadow = hasShadow || !!meta.sd
|
|
157
|
+
const wantShadow = hasShadow || !!meta.sd;
|
|
156
158
|
let root;
|
|
157
159
|
let isSSR;
|
|
158
160
|
if (hasShadow) {
|
|
@@ -208,7 +210,53 @@ export class WebUIElement extends HTMLElement {
|
|
|
208
210
|
}
|
|
209
211
|
hydrationEnd();
|
|
210
212
|
}
|
|
211
|
-
disconnectedCallback() {
|
|
213
|
+
disconnectedCallback() {
|
|
214
|
+
// Schedule teardown on microtask β if the element is re-connected
|
|
215
|
+
// before then (e.g. repeat reconciliation), skip the cleanup.
|
|
216
|
+
if (this.$root) {
|
|
217
|
+
queueMicrotask(() => {
|
|
218
|
+
if (!this.isConnected)
|
|
219
|
+
this.$destroy();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Permanently destroy this component's own bindings and DOM references.
|
|
225
|
+
* Each component is responsible for its own cleanup β child WebUI
|
|
226
|
+
* elements handle theirs via their own `disconnectedCallback`.
|
|
227
|
+
*/
|
|
228
|
+
$destroy() {
|
|
229
|
+
if (!this.$root)
|
|
230
|
+
return;
|
|
231
|
+
this.$teardown(this.$root);
|
|
232
|
+
this.$root = null;
|
|
233
|
+
this.$pathIndex = undefined;
|
|
234
|
+
this.$wildcardBindings = undefined;
|
|
235
|
+
this.$dirtyPaths = null;
|
|
236
|
+
this.$pendingFlush = false;
|
|
237
|
+
this.$ready = false;
|
|
238
|
+
}
|
|
239
|
+
/** Break all DOM references held by a binding instance and its nested blocks. */
|
|
240
|
+
$teardown(instance) {
|
|
241
|
+
for (const c of instance.conds) {
|
|
242
|
+
if (c.instance)
|
|
243
|
+
this.$teardown(c.instance);
|
|
244
|
+
c.instance = null;
|
|
245
|
+
}
|
|
246
|
+
for (const r of instance.repeats) {
|
|
247
|
+
for (const item of r.instances)
|
|
248
|
+
this.$teardown(item.instance);
|
|
249
|
+
r.instances.length = 0;
|
|
250
|
+
r.container = null;
|
|
251
|
+
r.start = null;
|
|
252
|
+
r.end = null;
|
|
253
|
+
}
|
|
254
|
+
instance.nodes.length = 0;
|
|
255
|
+
instance.texts.length = 0;
|
|
256
|
+
instance.attrs.length = 0;
|
|
257
|
+
instance.conds.length = 0;
|
|
258
|
+
instance.repeats.length = 0;
|
|
259
|
+
}
|
|
212
260
|
/** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
|
|
213
261
|
$emit(name, detail) {
|
|
214
262
|
return this.dispatchEvent(new CustomEvent(name, {
|
|
@@ -218,16 +266,18 @@ export class WebUIElement extends HTMLElement {
|
|
|
218
266
|
detail,
|
|
219
267
|
}));
|
|
220
268
|
}
|
|
221
|
-
/** Populate @observable properties from router state.
|
|
269
|
+
/** Populate @observable properties from server or router state.
|
|
222
270
|
*
|
|
223
271
|
* Each property is set through its reactive setter, which coalesces
|
|
224
272
|
* updates into a single pending microtask. We then synchronously
|
|
225
273
|
* flush those pending path updates so the DOM is current before any
|
|
226
274
|
* view-transition snapshot captures it.
|
|
227
275
|
*/
|
|
228
|
-
|
|
276
|
+
setState(state) {
|
|
229
277
|
const names = getObservableNames(this.constructor);
|
|
230
|
-
|
|
278
|
+
const keys = Object.keys(state);
|
|
279
|
+
for (let i = 0; i < keys.length; i++) {
|
|
280
|
+
const key = keys[i];
|
|
231
281
|
if (names.has(key)) {
|
|
232
282
|
this[key] = state[key];
|
|
233
283
|
}
|
|
@@ -235,18 +285,18 @@ export class WebUIElement extends HTMLElement {
|
|
|
235
285
|
this.$flushUpdates();
|
|
236
286
|
}
|
|
237
287
|
/**
|
|
238
|
-
* Apply SSR state from
|
|
288
|
+
* Apply SSR state from `window.__webui.state`.
|
|
239
289
|
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
290
|
+
* The handler emits all SSR metadata in a single consolidated
|
|
291
|
+
* `window.__webui` script block. State lives at `.state` β the same
|
|
292
|
+
* props passed to the server render so observables match the DOM.
|
|
293
|
+
* Only observable properties are set β unknown keys are ignored.
|
|
244
294
|
*
|
|
245
295
|
* Writes directly to the backing field (`_prop`) to avoid triggering
|
|
246
296
|
* reactive updates before bindings are wired.
|
|
247
297
|
*/
|
|
248
298
|
$applySSRState() {
|
|
249
|
-
const state = window.
|
|
299
|
+
const state = window.__webui?.state;
|
|
250
300
|
if (!state || typeof state !== 'object')
|
|
251
301
|
return;
|
|
252
302
|
const names = getObservableNames(this.constructor);
|
|
@@ -775,23 +825,25 @@ export class WebUIElement extends HTMLElement {
|
|
|
775
825
|
*/
|
|
776
826
|
$hydrateCondContent(condAnchor, blockMeta, scope) {
|
|
777
827
|
const rootTag = this.$rootTag(blockMeta);
|
|
778
|
-
|
|
828
|
+
const tplDom = getTemplateDom(blockMeta);
|
|
829
|
+
if (rootTag && tplDom.children.length === 1) {
|
|
830
|
+
// Single-root optimisation: hydrate the element in-place (pathStart=1).
|
|
779
831
|
const el = nextElement(condAnchor);
|
|
780
832
|
if (el) {
|
|
781
|
-
const inst = this.$hydrate(el, blockMeta,
|
|
833
|
+
const inst = this.$hydrate(el, blockMeta, tplDom, scope, 1);
|
|
782
834
|
this.$updateInstance(inst);
|
|
783
835
|
return inst;
|
|
784
836
|
}
|
|
785
837
|
return null;
|
|
786
838
|
}
|
|
787
|
-
//
|
|
839
|
+
// Multi-root or text-only conditional: collect nodes between <!--wc--> and <!--/wc-->
|
|
788
840
|
const condNodes = this.$collectBetween(condAnchor, MARKER_COND_END);
|
|
789
841
|
if (condNodes.length === 0)
|
|
790
842
|
return null;
|
|
791
843
|
const wrapper = document.createElement('div');
|
|
792
844
|
for (let cn = 0; cn < condNodes.length; cn++)
|
|
793
845
|
wrapper.appendChild(condNodes[cn]);
|
|
794
|
-
const inst = this.$hydrate(wrapper, blockMeta,
|
|
846
|
+
const inst = this.$hydrate(wrapper, blockMeta, tplDom, scope);
|
|
795
847
|
inst.nodes = childNodesArray(wrapper);
|
|
796
848
|
let afterNode = condAnchor;
|
|
797
849
|
for (let cn = 0; cn < inst.nodes.length; cn++) {
|
|
@@ -849,9 +901,14 @@ export class WebUIElement extends HTMLElement {
|
|
|
849
901
|
}
|
|
850
902
|
/** Extract root tag name from block metadata. */
|
|
851
903
|
$rootTag(meta) {
|
|
904
|
+
let cached = rootTagCache.get(meta);
|
|
905
|
+
if (cached !== undefined)
|
|
906
|
+
return cached;
|
|
852
907
|
const h = meta.h;
|
|
853
|
-
if (!h || h.charCodeAt(0) !== 60)
|
|
908
|
+
if (!h || h.charCodeAt(0) !== 60) {
|
|
909
|
+
rootTagCache.set(meta, null);
|
|
854
910
|
return null;
|
|
911
|
+
}
|
|
855
912
|
let end = 1;
|
|
856
913
|
while (end < h.length) {
|
|
857
914
|
const c = h.charCodeAt(end);
|
|
@@ -859,7 +916,9 @@ export class WebUIElement extends HTMLElement {
|
|
|
859
916
|
break;
|
|
860
917
|
end++;
|
|
861
918
|
}
|
|
862
|
-
|
|
919
|
+
const tag = h.slice(1, end).toLowerCase();
|
|
920
|
+
rootTagCache.set(meta, tag);
|
|
921
|
+
return tag;
|
|
863
922
|
}
|
|
864
923
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
865
924
|
// Shared: binding wiring, event wiring, refs
|
|
@@ -907,14 +966,11 @@ export class WebUIElement extends HTMLElement {
|
|
|
907
966
|
}
|
|
908
967
|
}
|
|
909
968
|
/** Attach a single event listener. */
|
|
910
|
-
$addEvent(target, eventName, handlerName,
|
|
969
|
+
$addEvent(target, eventName, handlerName, _needsEvent) {
|
|
911
970
|
const method = this[handlerName];
|
|
912
971
|
if (typeof method !== 'function')
|
|
913
972
|
return;
|
|
914
|
-
|
|
915
|
-
target.addEventListener(eventName, needsEvent
|
|
916
|
-
? (e) => method.call(self, e)
|
|
917
|
-
: () => method.call(self));
|
|
973
|
+
target.addEventListener(eventName, method.bind(this));
|
|
918
974
|
}
|
|
919
975
|
/** Find w-ref attributes and assign to component properties. */
|
|
920
976
|
$wireRefs(root) {
|
package/dist/template.d.ts
CHANGED
|
@@ -21,10 +21,12 @@ export type { CompiledAttrGroupMeta, CompiledAttrMeta, CompiledAttrPart, Compile
|
|
|
21
21
|
import type { TemplateMeta } from './template-types.js';
|
|
22
22
|
declare global {
|
|
23
23
|
interface Window {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/** Consolidated SSR bootstrap object β single script block. */
|
|
25
|
+
__webui?: {
|
|
26
|
+
state?: Record<string, unknown>;
|
|
27
|
+
templates?: Record<string, TemplateMeta>;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
export declare function getTemplate(name: string): TemplateMeta | undefined;
|
package/dist/template.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/webui-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "WebUI Framework Next β Preact-inspired lightweight Web Component runtime with SSR hydration. 15KB minified, compiled-template path mapping, no hydration markers.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"@playwright/test": "^1.58.2",
|
|
20
20
|
"@types/node": "^25.3.5",
|
|
21
21
|
"typescript": "^5.9.3",
|
|
22
|
-
"@microsoft/webui-test-support": "0.0.
|
|
22
|
+
"@microsoft/webui-test-support": "0.0.11"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|