@microsoft/webui-framework 0.0.16 → 0.0.18

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.
@@ -1,38 +1,14 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT license.
3
- /**
4
- * Hydration marker utilities for zero-DOM-mutation in-place hydration.
5
- *
6
- * The WebUI handler plugin emits lightweight HTML comment markers around
7
- * structural boundaries (for-loops and if-conditions). These utilities
8
- * walk markers and locate elements so the hydration path can wire
9
- * bindings in-place without reparenting DOM nodes.
10
- *
11
- * Marker format:
12
- * <!--wr--> repeat block start
13
- * <!--/wr--> repeat block end
14
- * <!--wi--> repeat item boundary
15
- * <!--wc--> conditional block start
16
- * <!--/wc--> conditional block end
17
- */
18
- // Marker data constants matching the handler plugin output.
19
1
  export const MARKER_REPEAT_START = 'wr';
20
2
  export const MARKER_REPEAT_END = '/wr';
21
3
  export const MARKER_COND_START = 'wc';
22
4
  export const MARKER_COND_END = '/wc';
23
5
  const MARKER_REPEAT_ITEM = 'wi';
24
- /**
25
- * Collect the item markers (<!--wi-->) within a repeat range.
26
- *
27
- * Walks siblings from the repeat start marker to the repeat end marker.
28
- * Returns an array of <!--wi--> comment nodes that delineate items.
29
- */
30
6
  export function collectItemMarkers(repeatStart) {
31
7
  const items = [];
32
8
  let end = null;
33
9
  let node = repeatStart.nextSibling;
34
10
  while (node) {
35
- if (node.nodeType === 8 /* COMMENT_NODE */) {
11
+ if (node.nodeType === 8) {
36
12
  const data = node.data;
37
13
  if (data === MARKER_REPEAT_END) {
38
14
  end = node;
@@ -45,16 +21,12 @@ export function collectItemMarkers(repeatStart) {
45
21
  }
46
22
  return { items, end };
47
23
  }
48
- /**
49
- * Get the next element sibling after a marker comment, skipping
50
- * whitespace text nodes and other comments.
51
- */
52
24
  export function nextElement(marker) {
53
25
  let node = marker.nextSibling;
54
26
  while (node) {
55
- if (node.nodeType === 1 /* ELEMENT_NODE */)
27
+ if (node.nodeType === 1)
56
28
  return node;
57
- if (node.nodeType === 8 /* COMMENT_NODE */) {
29
+ if (node.nodeType === 8) {
58
30
  const data = node.data;
59
31
  if (data === MARKER_REPEAT_END || data === MARKER_REPEAT_ITEM)
60
32
  return null;
@@ -63,38 +35,18 @@ export function nextElement(marker) {
63
35
  }
64
36
  return null;
65
37
  }
66
- /**
67
- * Find the Nth child of a given nodeType, skipping structural block ranges.
68
- *
69
- * The compiled template static HTML (`meta.h`) does not contain conditional
70
- * or repeat block content — those are stored as separate block metadata.
71
- * But the SSR DOM has this content rendered inline between marker pairs
72
- * (`<!--wc-->...<!--/wc-->` and `<!--wr-->...<!--/wr-->`).
73
- *
74
- * This function walks `parent.firstChild` → siblings, counting only
75
- * children of the requested `nodeType` that are NOT inside a structural
76
- * block range. Nested blocks of the same type are handled via depth
77
- * tracking. Returns the child at the given `ordinal`, or null.
78
- *
79
- * Used by `$resolveSSR` (element ordinals) and `$findSSRText` (text
80
- * ordinals) to keep SSR DOM ordinals aligned with template metadata.
81
- *
82
- * **Requires closing markers to still be in the DOM** — caller must
83
- * not remove `<!--/wc-->` or `<!--/wr-->` before all resolution is done.
84
- */
85
38
  export function findByOrdinal(parent, nodeType, ordinal) {
86
39
  let count = 0;
87
40
  let child = parent.firstChild;
88
41
  while (child) {
89
- // Detect a structural block opening marker and skip the entire range.
90
- if (child.nodeType === 8 /* COMMENT_NODE */) {
42
+ if (child.nodeType === 8) {
91
43
  const data = child.data;
92
44
  if (data === MARKER_COND_START || data === MARKER_REPEAT_START) {
93
45
  const endTag = data === MARKER_COND_START ? MARKER_COND_END : MARKER_REPEAT_END;
94
46
  let depth = 1;
95
47
  child = child.nextSibling;
96
48
  while (child && depth > 0) {
97
- if (child.nodeType === 8 /* COMMENT_NODE */) {
49
+ if (child.nodeType === 8) {
98
50
  const d = child.data;
99
51
  if (d === data)
100
52
  depth++;
@@ -104,7 +56,6 @@ export function findByOrdinal(parent, nodeType, ordinal) {
104
56
  if (depth > 0)
105
57
  child = child.nextSibling;
106
58
  }
107
- // Advance past the closing marker itself
108
59
  if (child)
109
60
  child = child.nextSibling;
110
61
  continue;
@@ -1,11 +1 @@
1
- /**
2
- * Adopt a CSS module stylesheet onto a shadow root, or inject into `<head>`
3
- * for light DOM components.
4
- *
5
- * For shadow DOM: uses `import(specifier, { with: { type: "css" } })` to
6
- * retrieve the browser-registered CSSStyleSheet from the module registry.
7
- * The browser caches the sheet internally — no application-level cache needed.
8
- *
9
- * For light DOM: appends a `<style>` element to `<head>` (once per specifier).
10
- */
11
1
  export declare function injectModuleStyle(specifier: string, shadowRoot: ShadowRoot | null): void;
@@ -1,78 +1,17 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT license.
3
- /**
4
- * Stylesheet management for WebUI components.
5
- *
6
- * Three CSS strategies are supported:
7
- *
8
- * - **Link**: `<link rel="stylesheet">` tags in each component's shadow template.
9
- * The browser deduplicates fetches by URL. No JS-side style management needed.
10
- *
11
- * - **Style**: Inline `<style>` tags inside each shadow template.
12
- *
13
- * - **Module**: Uses CSS Modules registered via Import Maps. During SSR, the
14
- * handler emits a `<script type="importmap">{"imports":{"<tag>":"data:text/css,..."}}</script>`
15
- * in each rendered component's light DOM. The browser registers the
16
- * stylesheet globally under `<tag>` and automatically adopts it via
17
- * `shadowrootadoptedstylesheets` on declarative shadow roots.
18
- *
19
- * During SPA navigation, the router appends new importmap script tags to
20
- * `<head>` via `templateStyles[]`. The framework uses
21
- * `import(specifier, { with: { type: "css" } })` to retrieve the browser's
22
- * registered CSSStyleSheet and adopts it onto the shadow root. This is a
23
- * direct hash-map lookup in the browser's module registry - no DOM queries,
24
- * no manual CSSStyleSheet construction.
25
- *
26
- * For light DOM components (no shadow root), Module mode injects a `<style>`
27
- * element in `<head>`, deduplicated by `headInjected`.
28
- */
29
- /**
30
- * Specifiers already injected into `<head>` via the light DOM path.
31
- * Prevents duplicate `<style>` elements for non-shadow components.
32
- */
33
1
  const headInjected = new Set();
34
- /**
35
- * Adopt a CSS module stylesheet onto a shadow root, or inject into `<head>`
36
- * for light DOM components.
37
- *
38
- * For shadow DOM: uses `import(specifier, { with: { type: "css" } })` to
39
- * retrieve the browser-registered CSSStyleSheet from the module registry.
40
- * The browser caches the sheet internally — no application-level cache needed.
41
- *
42
- * For light DOM: appends a `<style>` element to `<head>` (once per specifier).
43
- */
44
2
  export function injectModuleStyle(specifier, shadowRoot) {
45
3
  if (shadowRoot) {
46
- // SSR hydration: the browser already adopted the sheet from
47
- // shadowrootadoptedstylesheets on the declarative shadow root.
48
4
  if (shadowRoot.adoptedStyleSheets.length > 0)
49
5
  return;
50
- // SPA path: import the CSS module from the browser's registry.
51
- // The specifier was registered via a `<script type="importmap">` tag
52
- // (either inlined at SSR time or appended to <head> by the router during
53
- // partial navigation). The import resolves to the same CSSStyleSheet the
54
- // browser registered.
55
6
  import(specifier, { with: { type: 'css' } }).then((mod) => {
56
- shadowRoot.adoptedStyleSheets = [
57
- ...shadowRoot.adoptedStyleSheets,
58
- mod.default,
59
- ];
7
+ shadowRoot.adoptedStyleSheets.push(mod.default);
60
8
  }, () => {
61
- // Specifier not registered — component has no CSS module definition.
62
- // This is expected for Link/Style strategies or components without CSS.
63
9
  });
64
10
  }
65
11
  else if (!headInjected.has(specifier)) {
66
12
  headInjected.add(specifier);
67
13
  import(specifier, { with: { type: 'css' } }).then((mod) => {
68
- const style = document.createElement('style');
69
- const rules = mod.default.cssRules;
70
- let cssText = '';
71
- for (let i = 0; i < rules.length; i++) {
72
- cssText += rules[i].cssText;
73
- }
74
- style.textContent = cssText;
75
- document.head.appendChild(style);
14
+ document.adoptedStyleSheets.push(mod.default);
76
15
  }, () => { });
77
16
  }
78
17
  }
@@ -1,41 +1,16 @@
1
- /**
2
- * Shared runtime types for the WebUI element system.
3
- *
4
- * These types define the binding data structures that are created once during
5
- * hydration and then read on every reactive update. Each binding holds a
6
- * direct DOM node reference so that updates can patch the DOM in O(1) per
7
- * binding without any tree walking or selector queries.
8
- *
9
- * ## Key concepts
10
- *
11
- * - **TemplateInstance** — a connected block of DOM with its bindings. The
12
- * root component has one instance; conditionals and repeat items each get
13
- * their own nested instance.
14
- *
15
- * - **ScopeFrame** — a linked-list frame for repeat variable scoping.
16
- * `@for(item of items)` creates a frame `{ name: 'item', value, parent }`.
17
- * Nested repeats chain frames so inner bindings can resolve outer variables.
18
- */
19
- import type { CompiledAttrMeta, CompiledAttrPart, CompiledCondition } from '../template.js';
20
- /** Direct reference to a text node bound to a property path. */
1
+ import type { CompiledAttrPart, CompiledCondition } from '../template.js';
21
2
  export interface TextBinding {
22
3
  node: Text;
23
4
  path?: string;
24
5
  parts?: CompiledAttrPart[];
25
6
  scope?: ScopeFrame;
26
- /** When true, the binding renders unescaped HTML via innerHTML on the
27
- * parent element instead of setting Text.data. Corresponds to the
28
- * triple-brace `{{{expr}}}` template syntax. */
29
7
  raw?: boolean;
30
- /** The parent element for raw bindings — innerHTML is set here. */
31
8
  rawParent?: Element;
32
9
  }
33
- /** Attribute binding kind constants (matches compiled metadata). */
34
10
  export declare const ATTR_KIND_ATTRIBUTE = 0;
35
11
  export declare const ATTR_KIND_COMPLEX = 1;
36
12
  export declare const ATTR_KIND_BOOLEAN = 2;
37
13
  export declare const ATTR_KIND_TEMPLATE = 3;
38
- /** Direct reference to an attribute binding. */
39
14
  export interface AttrBinding {
40
15
  element: Element;
41
16
  name: string;
@@ -57,8 +32,8 @@ export interface TemplateInstance {
57
32
  attrs: AttrBinding[];
58
33
  conds: CondBinding[];
59
34
  repeats: RepeatBinding[];
35
+ cleanups?: Array<() => void>;
60
36
  }
61
- /** Direct reference to a conditional block with anchor + nested compiled block. */
62
37
  export interface CondBinding {
63
38
  condition: CompiledCondition;
64
39
  blockIndex: number;
@@ -66,7 +41,6 @@ export interface CondBinding {
66
41
  scope?: ScopeFrame;
67
42
  instance: TemplateInstance | null;
68
43
  }
69
- /** Repeat block tracking. */
70
44
  export interface RepeatBinding {
71
45
  markerId: number;
72
46
  collection: string;
@@ -79,9 +53,8 @@ export interface RepeatBinding {
79
53
  owner: TemplateInstance;
80
54
  instances: RepeatItemInstance[];
81
55
  rootTag: string | null;
82
- attrMap: Record<string, string>;
83
- rootBindings: CompiledAttrMeta[];
84
- /** Set to true once the collection has been explicitly set by client code. */
56
+ keyAttribute?: string;
57
+ keyPath?: string;
85
58
  synced?: boolean;
86
59
  }
87
60
  export interface RepeatItemInstance {
@@ -89,16 +62,8 @@ export interface RepeatItemInstance {
89
62
  value: unknown;
90
63
  instance: TemplateInstance;
91
64
  }
92
- /**
93
- * Minimal host interface for repeat operations.
94
- *
95
- * Repeat functions need access to host capabilities (value resolution,
96
- * block lookup, instance management) without depending on the full
97
- * WebUIElement class.
98
- */
99
65
  export interface RepeatHost {
100
66
  $resolveValue(path: string, scope?: ScopeFrame): unknown;
101
- /** Create, wire, and perform the first binding pass while detached. */
102
67
  $createBlockInstance(blockIndex: number, scope?: ScopeFrame): TemplateInstance | null;
103
68
  $updateInstance(instance: TemplateInstance): void;
104
69
  $removeInstance(instance: TemplateInstance): void;
@@ -1,6 +1,3 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT license.
3
- /** Attribute binding kind constants (matches compiled metadata). */
4
1
  export const ATTR_KIND_ATTRIBUTE = 0;
5
2
  export const ATTR_KIND_COMPLEX = 1;
6
3
  export const ATTR_KIND_BOOLEAN = 2;
package/dist/element.d.ts CHANGED
@@ -1,140 +1,22 @@
1
- import type { TemplateBlockMeta } from './template.js';
1
+ import { TemplateElement } from './template-element.js';
2
+ import type { TemplateBlockMeta, TemplateNodePath } from './template.js';
2
3
  import type { ScopeFrame, TemplateInstance } from './element/types.js';
3
- export declare class WebUIElement extends HTMLElement {
4
- private $root;
5
- private $meta?;
6
- private $ready;
7
- private $hydrated;
8
- private $dirtyPaths;
9
- private $pendingFlush;
10
- /** Cached condition resolver — avoids allocating a closure per evaluation. */
11
- private $resolver;
12
- private $pathIndex?;
13
- /** Bindings that reference non-observable paths — updated on every flush. */
14
- private $wildcardBindings?;
15
- static define(tagName: string): void;
16
- connectedCallback(): void;
17
- /** Mount the component after children are available. */
18
- private $mount;
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;
28
- /** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
4
+ export declare class WebUIElement extends TemplateElement {
5
+ protected $observableNames(): Set<string>;
6
+ protected $shouldApplySSRState(key: string): boolean;
7
+ protected $syncAuthoredAttributes(): void;
29
8
  $emit(name: string, detail?: unknown): boolean;
30
- /** Populate @observable properties from server or router state.
31
- *
32
- * Each property is set through its reactive setter, which coalesces
33
- * updates into a single pending microtask. We then synchronously
34
- * flush those pending path updates so the DOM is current before any
35
- * view-transition snapshot captures it.
36
- */
37
- setState(state: Record<string, unknown>): void;
38
- /**
39
- * Apply SSR state from `window.__webui.state`.
40
- *
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.
45
- *
46
- * Writes directly to the backing field (`_prop`) to avoid triggering
47
- * reactive updates before bindings are wired.
48
- */
49
- private $applySSRState;
50
- /** Reactive update — called by @observable/@attr setters. */
51
- $update(path?: string): void;
52
- /** Synchronously flush all queued path updates. Call this when you need
53
- * the DOM to reflect pending property changes immediately. */
54
- $flushUpdates(): void;
55
- /** Flush all queued path updates. Handles re-entrant setter calls. */
56
- private $flush;
57
- private $resolve;
58
- private $resolveSSR;
59
- private $parseTemplate;
60
- private $createStagingRoot;
61
- private $appendStagedChildren;
62
- private $releaseStagingRepeatContainers;
63
- private $wire;
64
- /**
65
- * Hydrate SSR-rendered DOM against compiled template metadata.
66
- *
67
- * When pathStart=0 (default): ssrRoot is a container with children
68
- * (top-level component hydration).
69
- *
70
- * When pathStart=1: ssrRoot is a block element itself (repeat item
71
- * in-place hydration). The leading [0] wrapper segment is skipped
72
- * so compiled paths resolve directly against the element.
73
- */
74
- private $hydrate;
75
- /** Collect sibling nodes between a start marker and an end marker comment. */
76
- private $collectBetween;
77
- /**
78
- * Hydrate a conditional block's content — shared by top-level and
79
- * repeat-item conditional hydration paths.
80
- */
81
- private $hydrateCondContent;
82
- /**
83
- * Find the next marker comment with the given data among a parent's children.
84
- * Starts searching from `after` (exclusive) if provided, or from firstChild.
85
- */
86
- private $findMarker;
87
- /**
88
- * Check whether there is non-marker content between a conditional
89
- * start anchor and its closing marker. Used during SSR hydration to
90
- * detect server-rendered conditional content even when the runtime
91
- * condition value has not been set yet (e.g. complex property from a
92
- * parent repeat binding that hydrates after its children).
93
- */
94
- private $hasContentAfterMarker;
95
- /**
96
- * Find existing SSR text node by mapping template text-node ordinal.
97
- *
98
- * Similar to `$resolveSSR`, the SSR DOM may contain extra text nodes
99
- * inside structural blocks (`<if>`/`<for>`) that are not in the
100
- * compiled template. We skip `<!--wc-->...<!--/wc-->` and
101
- * `<!--wr-->...<!--/wr-->` ranges to keep text ordinals aligned.
102
- */
103
- private $findSSRText;
104
- /** Find the SSR insertion reference for an empty text slot. */
105
- private $findSSRSlotRef;
106
- /** Extract root tag name from block metadata. */
107
- private $rootTag;
108
- /** Wire attribute bindings using a resolver (shared by $wire and $hydrate). */
109
- private $wireAttrs;
110
- /** Wire events + root events + refs (shared by $wire and $hydrate). */
111
- private $finalize;
112
- /** Wire events using a resolver function (works for both client and SSR). */
9
+ protected $finalize(instance: TemplateInstance, root: Node, meta: TemplateBlockMeta, resolver: (root: Node, path: TemplateNodePath) => Node | null, scope?: ScopeFrame): void;
113
10
  private $wireEvents;
114
- /** Wire root-level events on the host element (or shadow root when present). */
11
+ private $resolveDelegatedEvents;
115
12
  private $wireRoot;
116
- /** Attach a single event listener. */
13
+ private $addDelegatedEvent;
14
+ private $dispatchDelegatedEvent;
117
15
  private $addEvent;
16
+ private $addCleanup;
17
+ private $callEventHandler;
18
+ private $callEventHandlerWithCurrentTarget;
118
19
  private $resolveEventArgs;
119
20
  private $resolveEventArg;
120
- /** Find w-ref attributes and assign to component properties. */
121
21
  private $wireRefs;
122
- /** Create an AttrBinding from compiled metadata. */
123
- private $makeAttr;
124
- /** Build attrMap and rootBindings for a repeat block. */
125
- private $repeatMaps;
126
- private $buildPathIndex;
127
- private $updateBindings;
128
- $updateInstance(instance: TemplateInstance): void;
129
- private $patchText;
130
- private $patchAttr;
131
- private $toggleCond;
132
- $resolveValue(path: string, scope?: ScopeFrame): unknown;
133
- private $resolveParts;
134
- $block(blockIndex: number): TemplateBlockMeta | undefined;
135
- $createBlockInstance(blockIndex: number, scope?: ScopeFrame): TemplateInstance | null;
136
- $removeInstance(instance: TemplateInstance): void;
137
- $insertInstanceAfter(cursor: Node | null, container: ParentNode & Node, instance: TemplateInstance): Node | null;
138
- /** Extract the single dynamic path from a compiled attr parts array. */
139
- private $singleDynamic;
140
22
  }