@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 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.__webui_state` (like Preact's props)
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)** β€” see the [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity) for component authoring patterns.
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.__webui_templates`.
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` or `window.__webui_shadow` to decide)
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
- | `setInitialState(state, params?)` | Populate `@observable` properties from router state |
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 β”‚ β”‚ __webui_state JSON β”‚
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 &lt;script&gt;<br/>+ __webui_state &lt;script&gt;"]
332
+ R --> HTML["Full SSR HTML<br/>(shadow or light DOM)<br/>+ TemplateMeta &lt;script&gt;<br/>+ __webui.state &lt;script&gt;"]
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.__webui_state`
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/>+ __webui_state JSON
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 __webui_state
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.__webui_state` β€” a JSON object emitted by the
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["&lt;script&gt;<br/>window.__webui_state = {<br/> count: 42,<br/> title: 'Hello'<br/>}"] --> APPLY["$applySSRState()"]
563
+ SCRIPT["&lt;script&gt;<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.__webui_state`, so repeat items reflect the server-rendered list
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
- setInitialState(state: Record<string, unknown>): void;
37
+ setState(state: Record<string, unknown>): void;
30
38
  /**
31
- * Apply SSR state from the global `window.__webui_state` object.
39
+ * Apply SSR state from `window.__webui.state`.
32
40
  *
33
- * Passing the same props to both server render and client
34
- * hydrate, this ensures component observables match the server-rendered
35
- * DOM. The handler emits the state as a `<script>` tag at the end of
36
- * the page. Only observable properties are set β€” unknown keys are ignored.
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 __webui_templates.`);
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 || !!window.__webui_shadow;
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
- // Note: event listeners wired by $addEvent target child nodes owned by
213
- // this component β€” they will be GC'd together with the component.
214
- // We intentionally do NOT remove them here because connectedCallback
215
- // does not re-wire events when a hydrated component is reattached.
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
- setInitialState(state) {
276
+ setState(state) {
234
277
  const names = getObservableNames(this.constructor);
235
- for (const key of Object.keys(state)) {
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 the global `window.__webui_state` object.
288
+ * Apply SSR state from `window.__webui.state`.
244
289
  *
245
- * Passing the same props to both server render and client
246
- * hydrate, this ensures component observables match the server-rendered
247
- * DOM. The handler emits the state as a `<script>` tag at the end of
248
- * the page. Only observable properties are set β€” unknown keys are ignored.
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.__webui_state;
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
- return h.slice(1, end).toLowerCase();
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, needsEvent) {
969
+ $addEvent(target, eventName, handlerName, _needsEvent) {
918
970
  const method = this[handlerName];
919
971
  if (typeof method !== 'function')
920
972
  return;
921
- const self = this;
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) {
@@ -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
- __webui_templates?: Record<string, TemplateMeta>;
25
- __webui_state?: Record<string, unknown>;
26
- /** When true, all client-created components use shadow DOM. */
27
- __webui_shadow?: boolean;
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
@@ -1,5 +1,5 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT license.
3
3
  export function getTemplate(name) {
4
- return window.__webui_templates?.[name];
4
+ return window.__webui?.templates?.[name];
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/webui-framework",
3
- "version": "0.0.10",
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.10"
22
+ "@microsoft/webui-test-support": "0.0.11"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc",