@microsoft/webui-framework 0.0.10 β 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/element.d.ts +15 -7
- package/dist/element.js +71 -22
- 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/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
|
@@ -50,6 +50,8 @@ import { ATTR_KIND_BOOLEAN, ATTR_KIND_COMPLEX, ATTR_KIND_TEMPLATE, } from './ele
|
|
|
50
50
|
const templateCache = new WeakMap();
|
|
51
51
|
/** Parsed template DOM for SSR path mapping, keyed by TemplateBlockMeta. */
|
|
52
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();
|
|
@@ -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) {
|
|
@@ -209,10 +211,51 @@ export class WebUIElement extends HTMLElement {
|
|
|
209
211
|
hydrationEnd();
|
|
210
212
|
}
|
|
211
213
|
disconnectedCallback() {
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
|
|
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;
|
|
216
259
|
}
|
|
217
260
|
/** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
|
|
218
261
|
$emit(name, detail) {
|
|
@@ -223,16 +266,18 @@ export class WebUIElement extends HTMLElement {
|
|
|
223
266
|
detail,
|
|
224
267
|
}));
|
|
225
268
|
}
|
|
226
|
-
/** Populate @observable properties from router state.
|
|
269
|
+
/** Populate @observable properties from server or router state.
|
|
227
270
|
*
|
|
228
271
|
* Each property is set through its reactive setter, which coalesces
|
|
229
272
|
* updates into a single pending microtask. We then synchronously
|
|
230
273
|
* flush those pending path updates so the DOM is current before any
|
|
231
274
|
* view-transition snapshot captures it.
|
|
232
275
|
*/
|
|
233
|
-
|
|
276
|
+
setState(state) {
|
|
234
277
|
const names = getObservableNames(this.constructor);
|
|
235
|
-
|
|
278
|
+
const keys = Object.keys(state);
|
|
279
|
+
for (let i = 0; i < keys.length; i++) {
|
|
280
|
+
const key = keys[i];
|
|
236
281
|
if (names.has(key)) {
|
|
237
282
|
this[key] = state[key];
|
|
238
283
|
}
|
|
@@ -240,18 +285,18 @@ export class WebUIElement extends HTMLElement {
|
|
|
240
285
|
this.$flushUpdates();
|
|
241
286
|
}
|
|
242
287
|
/**
|
|
243
|
-
* Apply SSR state from
|
|
288
|
+
* Apply SSR state from `window.__webui.state`.
|
|
244
289
|
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
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.
|
|
249
294
|
*
|
|
250
295
|
* Writes directly to the backing field (`_prop`) to avoid triggering
|
|
251
296
|
* reactive updates before bindings are wired.
|
|
252
297
|
*/
|
|
253
298
|
$applySSRState() {
|
|
254
|
-
const state = window.
|
|
299
|
+
const state = window.__webui?.state;
|
|
255
300
|
if (!state || typeof state !== 'object')
|
|
256
301
|
return;
|
|
257
302
|
const names = getObservableNames(this.constructor);
|
|
@@ -856,9 +901,14 @@ export class WebUIElement extends HTMLElement {
|
|
|
856
901
|
}
|
|
857
902
|
/** Extract root tag name from block metadata. */
|
|
858
903
|
$rootTag(meta) {
|
|
904
|
+
let cached = rootTagCache.get(meta);
|
|
905
|
+
if (cached !== undefined)
|
|
906
|
+
return cached;
|
|
859
907
|
const h = meta.h;
|
|
860
|
-
if (!h || h.charCodeAt(0) !== 60)
|
|
908
|
+
if (!h || h.charCodeAt(0) !== 60) {
|
|
909
|
+
rootTagCache.set(meta, null);
|
|
861
910
|
return null;
|
|
911
|
+
}
|
|
862
912
|
let end = 1;
|
|
863
913
|
while (end < h.length) {
|
|
864
914
|
const c = h.charCodeAt(end);
|
|
@@ -866,7 +916,9 @@ export class WebUIElement extends HTMLElement {
|
|
|
866
916
|
break;
|
|
867
917
|
end++;
|
|
868
918
|
}
|
|
869
|
-
|
|
919
|
+
const tag = h.slice(1, end).toLowerCase();
|
|
920
|
+
rootTagCache.set(meta, tag);
|
|
921
|
+
return tag;
|
|
870
922
|
}
|
|
871
923
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
872
924
|
// Shared: binding wiring, event wiring, refs
|
|
@@ -914,14 +966,11 @@ export class WebUIElement extends HTMLElement {
|
|
|
914
966
|
}
|
|
915
967
|
}
|
|
916
968
|
/** Attach a single event listener. */
|
|
917
|
-
$addEvent(target, eventName, handlerName,
|
|
969
|
+
$addEvent(target, eventName, handlerName, _needsEvent) {
|
|
918
970
|
const method = this[handlerName];
|
|
919
971
|
if (typeof method !== 'function')
|
|
920
972
|
return;
|
|
921
|
-
|
|
922
|
-
target.addEventListener(eventName, needsEvent
|
|
923
|
-
? (e) => method.call(self, e)
|
|
924
|
-
: () => method.call(self));
|
|
973
|
+
target.addEventListener(eventName, method.bind(this));
|
|
925
974
|
}
|
|
926
975
|
/** Find w-ref attributes and assign to component properties. */
|
|
927
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",
|