@microsoft/webui-framework 0.0.5 → 0.0.7

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,95 +1,137 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+ /**
4
+ * Reactive decorators for WebUIElement properties.
5
+ *
6
+ * Uses legacy/experimental TypeScript decorators (`experimentalDecorators: true`)
7
+ * for compatibility with the FAST ecosystem conventions.
8
+ */
9
+ // ---------------------------------------------------------------------------
10
+ // Internal helpers
11
+ // ---------------------------------------------------------------------------
12
+ /** Convert camelCase to kebab-case for attribute reflection. */
1
13
  function toKebabCase(str) {
2
- return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
14
+ return str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
3
15
  }
16
+ /**
17
+ * Shared logic for installing a reactive getter/setter on a class prototype.
18
+ * The backing value is stored in a private `_prop` field on the instance.
19
+ */
4
20
  function createReactiveProperty(proto, name) {
5
- const backingKey = `_${name}`;
6
- const changedKey = `${name}Changed`;
7
- Object.defineProperty(proto, name, {
8
- get() {
9
- return this[backingKey];
10
- },
11
- set(newValue) {
12
- const oldValue = this[backingKey];
13
- if (oldValue === newValue) return;
14
- this[backingKey] = newValue;
15
- const cb = this[changedKey];
16
- if (typeof cb === "function") {
17
- cb.call(this, oldValue, newValue);
18
- }
19
- if (this.isConnected) {
20
- const upd = this["$update"];
21
- if (upd) upd.call(this, name);
22
- }
23
- },
24
- enumerable: true,
25
- configurable: true
26
- });
21
+ const backingKey = `_${name}`;
22
+ const changedKey = `${name}Changed`;
23
+ Object.defineProperty(proto, name, {
24
+ get() {
25
+ return this[backingKey];
26
+ },
27
+ set(newValue) {
28
+ const oldValue = this[backingKey];
29
+ if (oldValue === newValue)
30
+ return;
31
+ this[backingKey] = newValue;
32
+ const cb = this[changedKey];
33
+ if (typeof cb === 'function') {
34
+ cb.call(this, oldValue, newValue);
35
+ }
36
+ if (this.isConnected) {
37
+ const upd = this['$update'];
38
+ if (upd)
39
+ upd.call(this, name);
40
+ }
41
+ },
42
+ enumerable: true,
43
+ configurable: true,
44
+ });
27
45
  }
28
- const observableRegistry = /* @__PURE__ */ new WeakMap();
29
- const EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
30
- function getObservableNames(ctor) {
31
- return observableRegistry.get(ctor) ?? EMPTY_SET;
46
+ // ---------------------------------------------------------------------------
47
+ // @observable
48
+ // ---------------------------------------------------------------------------
49
+ /** Per-class registry of @observable property names. */
50
+ const observableRegistry = new WeakMap();
51
+ /**
52
+ * Get the set of @observable property names registered for a class.
53
+ */
54
+ const EMPTY_SET = Object.freeze(new Set());
55
+ export function getObservableNames(ctor) {
56
+ return observableRegistry.get(ctor) ?? EMPTY_SET;
32
57
  }
33
- function observable(target, name) {
34
- const ctor = target.constructor;
35
- if (!observableRegistry.has(ctor)) {
36
- observableRegistry.set(ctor, /* @__PURE__ */ new Set());
37
- }
38
- observableRegistry.get(ctor).add(name);
39
- createReactiveProperty(target, name);
58
+ /**
59
+ * Marks a property as observable. When the value changes the decorator will:
60
+ * 1. Call `this.<prop>Changed(oldValue, newValue)` if defined.
61
+ * 2. Call `this.$update(name)` if the element is connected, targeting
62
+ * only bindings that reference this property.
63
+ */
64
+ export function observable(target, name) {
65
+ const ctor = target.constructor;
66
+ if (!observableRegistry.has(ctor)) {
67
+ observableRegistry.set(ctor, new Set());
68
+ }
69
+ observableRegistry.get(ctor).add(name);
70
+ createReactiveProperty(target, name);
40
71
  }
41
- const attrMap = /* @__PURE__ */ new WeakMap();
42
- const boolAttrs = /* @__PURE__ */ new WeakMap();
72
+ // ---------------------------------------------------------------------------
73
+ // @attr
74
+ // ---------------------------------------------------------------------------
75
+ /**
76
+ * Registry of attribute-name → property-name mappings per constructor.
77
+ * Used by `attributeChangedCallback` to route attribute changes to properties.
78
+ */
79
+ const attrMap = new WeakMap();
80
+ /** Registry of boolean-mode attribute names per constructor. */
81
+ const boolAttrs = new WeakMap();
43
82
  function applyAttr(target, name, options) {
44
- const proto = target;
45
- const ctor = proto.constructor;
46
- createReactiveProperty(proto, name);
47
- const attrName = options?.attribute ?? toKebabCase(name);
48
- if (!attrMap.has(ctor)) {
49
- attrMap.set(ctor, /* @__PURE__ */ new Map());
50
- }
51
- attrMap.get(ctor).set(attrName, name);
52
- if (options?.mode === "boolean") {
53
- if (!boolAttrs.has(ctor)) {
54
- boolAttrs.set(ctor, /* @__PURE__ */ new Set());
83
+ const proto = target;
84
+ const ctor = proto.constructor;
85
+ // 1. Install the reactive getter/setter (same as @observable).
86
+ createReactiveProperty(proto, name);
87
+ // 2. Register the attribute mapping.
88
+ const attrName = options?.attribute ?? toKebabCase(name);
89
+ if (!attrMap.has(ctor)) {
90
+ attrMap.set(ctor, new Map());
55
91
  }
56
- boolAttrs.get(ctor).add(attrName);
57
- }
58
- if (!ctor._observedAttrs) {
59
- ctor._observedAttrs = [];
60
- Object.defineProperty(ctor, "observedAttributes", {
61
- get() {
62
- return ctor._observedAttrs ?? [];
63
- },
64
- configurable: true
65
- });
66
- const origACB = proto["attributeChangedCallback"];
67
- proto["attributeChangedCallback"] = function(attribute, oldVal, newVal) {
68
- const map = attrMap.get(this.constructor);
69
- const propName = map?.get(attribute);
70
- if (propName !== void 0) {
71
- const isBool = boolAttrs.get(this.constructor)?.has(attribute);
72
- this[propName] = isBool ? newVal !== null : newVal;
73
- }
74
- if (origACB) {
75
- origACB.call(this, attribute, oldVal, newVal);
76
- }
77
- };
78
- }
79
- ctor._observedAttrs.push(attrName);
92
+ attrMap.get(ctor).set(attrName, name);
93
+ // Track boolean-mode attrs.
94
+ if (options?.mode === 'boolean') {
95
+ if (!boolAttrs.has(ctor)) {
96
+ boolAttrs.set(ctor, new Set());
97
+ }
98
+ boolAttrs.get(ctor).add(attrName);
99
+ }
100
+ // 3. Accumulate observed attributes on the constructor.
101
+ if (!ctor._observedAttrs) {
102
+ ctor._observedAttrs = [];
103
+ // Define the static getter that `customElements.define` inspects.
104
+ Object.defineProperty(ctor, 'observedAttributes', {
105
+ get() {
106
+ return ctor._observedAttrs ?? [];
107
+ },
108
+ configurable: true,
109
+ });
110
+ // Patch `attributeChangedCallback` once per class.
111
+ const origACB = proto['attributeChangedCallback'];
112
+ proto['attributeChangedCallback'] = function (attribute, oldVal, newVal) {
113
+ // Route the attribute change to the corresponding property.
114
+ const map = attrMap.get(this.constructor);
115
+ const propName = map?.get(attribute);
116
+ if (propName !== undefined) {
117
+ const isBool = boolAttrs.get(this.constructor)?.has(attribute);
118
+ this[propName] = isBool ? newVal !== null : newVal;
119
+ }
120
+ // Preserve any pre-existing attributeChangedCallback.
121
+ if (origACB) {
122
+ origACB.call(this, attribute, oldVal, newVal);
123
+ }
124
+ };
125
+ }
126
+ ctor._observedAttrs.push(attrName);
80
127
  }
81
- function attr(targetOrOptions, name) {
82
- if (typeof name === "string") {
83
- applyAttr(targetOrOptions, name);
84
- return;
85
- }
86
- const options = targetOrOptions;
87
- return (target, propName) => {
88
- applyAttr(target, propName, options);
89
- };
128
+ export function attr(targetOrOptions, name) {
129
+ if (typeof name === 'string') {
130
+ applyAttr(targetOrOptions, name);
131
+ return;
132
+ }
133
+ const options = targetOrOptions;
134
+ return (target, propName) => {
135
+ applyAttr(target, propName, options);
136
+ };
90
137
  }
91
- export {
92
- attr,
93
- getObservableNames,
94
- observable
95
- };
@@ -1,117 +1,160 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT license.
3
+ // ── Helpers ─────────────────────────────────────────────────────────
1
4
  function asParent(node) {
2
- if (!node) return null;
3
- return "childNodes" in node ? node : null;
5
+ if (!node)
6
+ return null;
7
+ return 'childNodes' in node ? node : null;
4
8
  }
5
- function dotWalk(cursor, path, from) {
6
- let start = from;
7
- for (let i = from; i <= path.length; i++) {
8
- if (i === path.length || path.charCodeAt(i) === 46) {
9
- if (cursor == null || typeof cursor !== "object") return void 0;
10
- cursor = cursor[path.slice(start, i)];
11
- start = i + 1;
9
+ /** Resolve a dotted path from a start offset without allocating. */
10
+ export function dotWalk(cursor, path, from) {
11
+ let start = from;
12
+ for (let i = from; i <= path.length; i++) {
13
+ if (i === path.length || path.charCodeAt(i) === 46 /* . */) {
14
+ if (cursor == null || typeof cursor !== 'object')
15
+ return undefined;
16
+ cursor = cursor[path.slice(start, i)];
17
+ start = i + 1;
18
+ }
12
19
  }
13
- }
14
- return cursor;
20
+ return cursor;
15
21
  }
16
- function resolveRepeatValue(scopeVar, scope, path) {
17
- if (path === scopeVar) return scope;
18
- if (!path.startsWith(`${scopeVar}.`)) return void 0;
19
- return dotWalk(scope, path, scopeVar.length + 1);
22
+ /**
23
+ * Resolve a dotted path against a repeat scope variable.
24
+ *
25
+ * When a binding inside `@for(item of items)` references `item.title`,
26
+ * this function looks up `title` on the current scope value.
27
+ */
28
+ export function resolveRepeatValue(scopeVar, scope, path) {
29
+ if (path === scopeVar)
30
+ return scope;
31
+ if (!path.startsWith(`${scopeVar}.`))
32
+ return undefined;
33
+ return dotWalk(scope, path, scopeVar.length + 1);
20
34
  }
35
+ /** Compute a key for an item using the cached key path, or null. */
21
36
  function itemKey(item, keyPath) {
22
- if (keyPath === void 0 || keyPath === "") return null;
23
- const v = dotWalk(item, keyPath, 0);
24
- return v != null ? String(v) : "";
37
+ if (keyPath === undefined || keyPath === '')
38
+ return null;
39
+ const v = dotWalk(item, keyPath, 0);
40
+ return v != null ? String(v) : '';
25
41
  }
42
+ /** Build a scope frame for a repeat item. */
26
43
  function itemScope(rep, item) {
27
- return { name: rep.itemVar, value: item, parent: rep.scope };
44
+ return { name: rep.itemVar, value: item, parent: rep.scope };
28
45
  }
29
- function syncRepeat(host, rep) {
30
- const resolved = host.$resolveValue(rep.collection, rep.scope);
31
- const items = Array.isArray(resolved) ? resolved : [];
32
- let container = rep.container ?? (rep.start ? asParent(rep.start.parentNode) : null) ?? (rep.owner.nodes[0] ? asParent(rep.owner.nodes[0].parentNode) : null);
33
- if (!container) return;
34
- rep.container = container;
35
- if (!rep.synced && items.length === 0 && rep.instances.length > 0) return;
36
- rep.synced = true;
37
- if (items.length === 0) {
38
- for (let i = 0; i < rep.instances.length; i += 1) {
39
- host.$removeInstance(rep.instances[i].instance);
46
+ // ── Reconciliation ──────────────────────────────────────────────────
47
+ /**
48
+ * Reconcile a repeat binding against its current collection value.
49
+ *
50
+ * Called by `$updateInstance` on every reactive update. Resolves the
51
+ * collection path, diffs old vs. new items by key, and patches the DOM.
52
+ */
53
+ export function syncRepeat(host, rep) {
54
+ const resolved = host.$resolveValue(rep.collection, rep.scope);
55
+ const items = Array.isArray(resolved) ? resolved : [];
56
+ // Locate the container once and cache it.
57
+ let container = rep.container
58
+ ?? (rep.start ? asParent(rep.start.parentNode) : null)
59
+ ?? (rep.owner.nodes[0] ? asParent(rep.owner.nodes[0].parentNode) : null);
60
+ if (!container)
61
+ return;
62
+ rep.container = container;
63
+ // Before the first client-side sync, bail if the collection hasn't
64
+ // been explicitly set but SSR children already exist.
65
+ if (!rep.synced && items.length === 0 && rep.instances.length > 0)
66
+ return;
67
+ rep.synced = true;
68
+ // If there are no items, just tear down everything.
69
+ if (items.length === 0) {
70
+ for (let i = 0; i < rep.instances.length; i += 1) {
71
+ host.$removeInstance(rep.instances[i].instance);
72
+ }
73
+ rep.instances = [];
74
+ return;
40
75
  }
41
- rep.instances = [];
42
- return;
43
- }
44
- const keyPath = Object.values(rep.attrMap)[0];
45
- const hasKeys = keyPath !== void 0 && keyPath !== "";
46
- const oldInstances = rep.instances;
47
- if (!hasKeys) {
48
- const next2 = [];
49
- const reuseCount = Math.min(oldInstances.length, items.length);
50
- for (let i = 0; i < reuseCount; i += 1) {
51
- const entry = oldInstances[i];
52
- entry.value = items[i];
53
- if (entry.instance.scope) entry.instance.scope.value = items[i];
54
- next2.push(entry);
76
+ const keyPath = Object.values(rep.attrMap)[0];
77
+ const hasKeys = keyPath !== undefined && keyPath !== '';
78
+ const oldInstances = rep.instances;
79
+ // ── Fast path for unkeyed (index-based) repeats ────────────────
80
+ if (!hasKeys) {
81
+ const next = [];
82
+ const reuseCount = Math.min(oldInstances.length, items.length);
83
+ // Reuse existing instances by index
84
+ for (let i = 0; i < reuseCount; i += 1) {
85
+ const entry = oldInstances[i];
86
+ entry.value = items[i];
87
+ if (entry.instance.scope)
88
+ entry.instance.scope.value = items[i];
89
+ next.push(entry);
90
+ }
91
+ // Create new instances for items beyond old length
92
+ for (let i = reuseCount; i < items.length; i += 1) {
93
+ const scope = itemScope(rep, items[i]);
94
+ const instance = host.$createBlockInstance(rep.blockIndex, scope);
95
+ if (instance) {
96
+ next.push({ key: null, value: items[i], instance });
97
+ }
98
+ }
99
+ // Remove excess old instances
100
+ for (let i = reuseCount; i < oldInstances.length; i += 1) {
101
+ host.$removeInstance(oldInstances[i].instance);
102
+ }
103
+ rep.instances = next;
104
+ // Reorder + update
105
+ let cursor = rep.start;
106
+ for (let i = 0; i < next.length; i += 1) {
107
+ cursor = host.$insertInstanceAfter(cursor, container, next[i].instance);
108
+ }
109
+ for (let i = 0; i < next.length; i += 1) {
110
+ host.$updateInstance(next[i].instance);
111
+ }
112
+ return;
55
113
  }
56
- for (let i = reuseCount; i < items.length; i += 1) {
57
- const scope = itemScope(rep, items[i]);
58
- const instance = host.$createBlockInstance(rep.blockIndex, scope);
59
- if (instance) {
60
- next2.push({ key: null, value: items[i], instance });
61
- }
114
+ // ── Keyed diff ─────────────────────────────────────────────────
115
+ // ── Build old-key → instance map ────────────────────────────────
116
+ const oldByKey = new Map();
117
+ for (let i = 0; i < oldInstances.length; i += 1) {
118
+ const entry = oldInstances[i];
119
+ const k = entry.key;
120
+ if (k != null)
121
+ oldByKey.set(k, entry);
62
122
  }
63
- for (let i = reuseCount; i < oldInstances.length; i += 1) {
64
- host.$removeInstance(oldInstances[i].instance);
123
+ // ── Match / create ──────────────────────────────────────────────
124
+ const next = [];
125
+ for (let i = 0; i < items.length; i += 1) {
126
+ const item = items[i];
127
+ const key = itemKey(item, keyPath);
128
+ const existing = key != null ? oldByKey.get(key) : undefined;
129
+ if (existing) {
130
+ oldByKey.delete(key);
131
+ existing.value = item;
132
+ existing.key = key;
133
+ if (existing.instance.scope)
134
+ existing.instance.scope.value = item;
135
+ next.push(existing);
136
+ }
137
+ else {
138
+ const scope = itemScope(rep, item);
139
+ const instance = host.$createBlockInstance(rep.blockIndex, scope);
140
+ if (instance) {
141
+ next.push({ key: key ?? null, value: item, instance });
142
+ }
143
+ }
65
144
  }
66
- rep.instances = next2;
67
- let cursor2 = rep.start;
68
- for (let i = 0; i < next2.length; i += 1) {
69
- cursor2 = host.$insertInstanceAfter(cursor2, container, next2[i].instance);
145
+ // ── Remove unmatched old instances ──────────────────────────────
146
+ for (const leftover of oldByKey.values()) {
147
+ host.$removeInstance(leftover.instance);
70
148
  }
71
- for (let i = 0; i < next2.length; i += 1) {
72
- host.$updateInstance(next2[i].instance);
149
+ rep.instances = next;
150
+ // ── Reorder DOM (forward pass) ──────────────────────────────────
151
+ // Walk forward, skip nodes already in position.
152
+ let cursor = rep.start;
153
+ for (let i = 0; i < next.length; i += 1) {
154
+ cursor = host.$insertInstanceAfter(cursor, container, next[i].instance);
73
155
  }
74
- return;
75
- }
76
- const oldByKey = /* @__PURE__ */ new Map();
77
- for (let i = 0; i < oldInstances.length; i += 1) {
78
- const entry = oldInstances[i];
79
- const k = entry.key;
80
- if (k != null) oldByKey.set(k, entry);
81
- }
82
- const next = [];
83
- for (let i = 0; i < items.length; i += 1) {
84
- const item = items[i];
85
- const key = itemKey(item, keyPath);
86
- const existing = key != null ? oldByKey.get(key) : void 0;
87
- if (existing) {
88
- oldByKey.delete(key);
89
- existing.value = item;
90
- existing.key = key;
91
- if (existing.instance.scope) existing.instance.scope.value = item;
92
- next.push(existing);
93
- } else {
94
- const scope = itemScope(rep, item);
95
- const instance = host.$createBlockInstance(rep.blockIndex, scope);
96
- if (instance) {
97
- next.push({ key: key ?? null, value: item, instance });
98
- }
156
+ // ── Update bindings ─────────────────────────────────────────────
157
+ for (let i = 0; i < next.length; i += 1) {
158
+ host.$updateInstance(next[i].instance);
99
159
  }
100
- }
101
- for (const leftover of oldByKey.values()) {
102
- host.$removeInstance(leftover.instance);
103
- }
104
- rep.instances = next;
105
- let cursor = rep.start;
106
- for (let i = 0; i < next.length; i += 1) {
107
- cursor = host.$insertInstanceAfter(cursor, container, next[i].instance);
108
- }
109
- for (let i = 0; i < next.length; i += 1) {
110
- host.$updateInstance(next[i].instance);
111
- }
112
160
  }
113
- export {
114
- dotWalk,
115
- resolveRepeatValue,
116
- syncRepeat
117
- };
@@ -32,3 +32,23 @@ export declare function collectItemMarkers(repeatStart: Comment): {
32
32
  * whitespace text nodes and other comments.
33
33
  */
34
34
  export declare function nextElement(marker: Comment): Element | null;
35
+ /**
36
+ * Find the Nth child of a given nodeType, skipping structural block ranges.
37
+ *
38
+ * The compiled template static HTML (`meta.h`) does not contain conditional
39
+ * or repeat block content — those are stored as separate block metadata.
40
+ * But the SSR DOM has this content rendered inline between marker pairs
41
+ * (`<!--wc-->...<!--/wc-->` and `<!--wr-->...<!--/wr-->`).
42
+ *
43
+ * This function walks `parent.firstChild` → siblings, counting only
44
+ * children of the requested `nodeType` that are NOT inside a structural
45
+ * block range. Nested blocks of the same type are handled via depth
46
+ * tracking. Returns the child at the given `ordinal`, or null.
47
+ *
48
+ * Used by `$resolveSSR` (element ordinals) and `$findSSRText` (text
49
+ * ordinals) to keep SSR DOM ordinals aligned with template metadata.
50
+ *
51
+ * **Requires closing markers to still be in the DOM** — caller must
52
+ * not remove `<!--/wc-->` or `<!--/wr-->` before all resolution is done.
53
+ */
54
+ export declare function findByOrdinal(parent: Node, nodeType: number, ordinal: number): Node | null;