@microsoft/webui-framework 0.0.17 → 0.0.19

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,62 +1,12 @@
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
- */
30
1
  export declare function toKebabCase(str: string): string;
31
2
  export declare function getObservableNames(ctor: Function): Set<string>;
32
- /**
33
- * Marks a property as observable. When the value changes the decorator will:
34
- * 1. Call `this.<prop>Changed(oldValue, newValue)` if defined.
35
- * 2. Call `this.$update(name)` if the element is connected, targeting
36
- * only bindings that reference this property.
37
- */
38
- export declare function observable(target: object, name: string): void;
39
3
  export declare function isAttributeProperty(ctor: Function, property: string): boolean;
40
- /**
41
- * Like {@link observable} but also reflects to/from an HTML attribute
42
- * (kebab-case). The decorator patches `observedAttributes` and
43
- * `attributeChangedCallback` on the class so changes flow in both directions.
44
- *
45
- * @example
46
- * ```ts
47
- * class MyEl extends WebUIElement {
48
- * @attr myProp = 'default';
49
- * // syncs with attribute "my-prop"
50
- * }
51
- * ```
52
- */
4
+ export declare function attributeNameForProperty(ctor: Function, property: string): string | undefined;
5
+ export declare function syncAttrProperties(instance: object, ctor: Function): void;
6
+ export declare function observable(target: object, name: string): void;
53
7
  export interface AttrOptions {
54
8
  attribute?: string;
55
- /** When `'boolean'`, the property is `true` when the attribute is present
56
- * and `false` when absent — matching native HTML boolean attribute semantics.
57
- * Default is string mode (property receives the attribute string value). */
58
9
  mode?: 'boolean';
59
10
  }
60
- export declare function syncAttrProperties(instance: object, ctor: Function): void;
61
11
  export declare function attr(target: object, name: string): void;
62
12
  export declare function attr(options: AttrOptions): (target: object, name: string) => void;
@@ -1,23 +1,4 @@
1
- // Copyright (c) Microsoft Corporation.
2
- // Licensed under the MIT license.
3
- /**
4
- * Reactive decorators for WebUIElement properties.
5
- *
6
- * Uses TypeScript's `experimentalDecorators` emit, matching the FAST ecosystem
7
- * conventions.
8
- */
9
- // ---------------------------------------------------------------------------
10
- // Internal helpers
11
- // ---------------------------------------------------------------------------
12
- /**
13
- * Map of camelCase property names to their HTML attribute names.
14
- *
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.
18
- */
19
1
  const propertyToAttribute = Object.assign(Object.create(null), {
20
- // --- HTML global/element attributes ---
21
2
  accessKey: 'accesskey',
22
3
  autoCapitalize: 'autocapitalize',
23
4
  contentEditable: 'contenteditable',
@@ -40,41 +21,11 @@ const propertyToAttribute = Object.assign(Object.create(null), {
40
21
  tabIndex: 'tabindex',
41
22
  useMap: 'usemap',
42
23
  });
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
- */
72
24
  export function toKebabCase(str) {
73
25
  const mapped = propertyToAttribute[str];
74
26
  if (mapped)
75
27
  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) {
28
+ if (str.length > 4 && str.charCodeAt(0) === 97 && str.startsWith('aria') && str.charCodeAt(4) >= 65 && str.charCodeAt(4) <= 90) {
78
29
  return 'aria-' + str.slice(4).toLowerCase();
79
30
  }
80
31
  let out = '';
@@ -85,96 +36,12 @@ export function toKebabCase(str) {
85
36
  return out;
86
37
  }
87
38
  const reflectingAttribute = Symbol('webui.reflectingAttribute');
39
+ const EMPTY_SET = Object.freeze(new Set());
88
40
  function parentConstructor(ctor) {
89
41
  const parent = Object.getPrototypeOf(ctor);
90
42
  return typeof parent === 'function' && parent !== Function.prototype ? parent : null;
91
43
  }
92
- function createReactiveProperty(proto, name, attrDefinition) {
93
- const backingKey = `_${name}`;
94
- const changedKey = `${name}Changed`;
95
- Object.defineProperty(proto, name, {
96
- get() {
97
- return this[backingKey];
98
- },
99
- set(newValue) {
100
- const oldValue = this[backingKey];
101
- if (Object.is(oldValue, newValue))
102
- return;
103
- this[backingKey] = newValue;
104
- if (attrDefinition && this['$ready'] === true) {
105
- reflectPropertyToAttribute(this, attrDefinition, newValue);
106
- }
107
- const cb = this[changedKey];
108
- if (typeof cb === 'function') {
109
- cb.call(this, oldValue, newValue);
110
- }
111
- if (this.isConnected) {
112
- const upd = this['$update'];
113
- if (upd)
114
- upd.call(this, name);
115
- }
116
- },
117
- enumerable: true,
118
- configurable: true,
119
- });
120
- }
121
- function reflectPropertyToAttribute(instance, definition, value) {
122
- const element = instance;
123
- const attrName = definition.attribute;
124
- if (definition.boolean) {
125
- const shouldHaveAttribute = Boolean(value);
126
- if (element.hasAttribute(attrName) === shouldHaveAttribute)
127
- return;
128
- setReflectingAttribute(instance, attrName);
129
- try {
130
- if (shouldHaveAttribute)
131
- element.setAttribute(attrName, '');
132
- else
133
- element.removeAttribute(attrName);
134
- }
135
- finally {
136
- restoreReflectingAttribute(instance);
137
- }
138
- return;
139
- }
140
- if (value == null) {
141
- if (!element.hasAttribute(attrName))
142
- return;
143
- setReflectingAttribute(instance, attrName);
144
- try {
145
- element.removeAttribute(attrName);
146
- }
147
- finally {
148
- restoreReflectingAttribute(instance);
149
- }
150
- return;
151
- }
152
- const attrValue = typeof value === 'string' ? value : String(value);
153
- if (element.getAttribute(attrName) === attrValue)
154
- return;
155
- setReflectingAttribute(instance, attrName);
156
- try {
157
- element.setAttribute(attrName, attrValue);
158
- }
159
- finally {
160
- restoreReflectingAttribute(instance);
161
- }
162
- }
163
- function setReflectingAttribute(instance, attrName) {
164
- instance[reflectingAttribute] = attrName;
165
- }
166
- function restoreReflectingAttribute(instance) {
167
- instance[reflectingAttribute] = undefined;
168
- }
169
- // ---------------------------------------------------------------------------
170
- // @observable
171
- // ---------------------------------------------------------------------------
172
- /** Per-class registry of @observable property names. */
173
44
  const observableRegistry = new WeakMap();
174
- /**
175
- * Get the set of @observable property names registered for a class.
176
- */
177
- const EMPTY_SET = Object.freeze(new Set());
178
45
  export function getObservableNames(ctor) {
179
46
  const names = observableRegistry.get(ctor);
180
47
  if (names)
@@ -192,26 +59,7 @@ function registerObservableProperty(ctor, name) {
192
59
  }
193
60
  names.add(name);
194
61
  }
195
- /**
196
- * Marks a property as observable. When the value changes the decorator will:
197
- * 1. Call `this.<prop>Changed(oldValue, newValue)` if defined.
198
- * 2. Call `this.$update(name)` if the element is connected, targeting
199
- * only bindings that reference this property.
200
- */
201
- export function observable(target, name) {
202
- const ctor = target.constructor;
203
- registerObservableProperty(ctor, name);
204
- createReactiveProperty(target, name);
205
- }
206
- // ---------------------------------------------------------------------------
207
- // @attr
208
- // ---------------------------------------------------------------------------
209
- /**
210
- * Registry of attribute-name → property-name mappings per constructor.
211
- * Used by `attributeChangedCallback` to route attribute changes to properties.
212
- */
213
62
  const attrByAttribute = new WeakMap();
214
- /** Registry of property-name → attribute metadata, used for mount-time sync. */
215
63
  const attrByProperty = new WeakMap();
216
64
  function inheritedAttrMap(registry, ctor) {
217
65
  let current = parentConstructor(ctor);
@@ -246,6 +94,100 @@ function attrPropertyMapFor(ctor) {
246
94
  export function isAttributeProperty(ctor, property) {
247
95
  return attrPropertyMapFor(ctor)?.has(property) === true;
248
96
  }
97
+ export function attributeNameForProperty(ctor, property) {
98
+ return attrPropertyMapFor(ctor)?.get(property)?.attribute;
99
+ }
100
+ function setReflectingAttribute(instance, attrName) {
101
+ instance[reflectingAttribute] = attrName;
102
+ }
103
+ function restoreReflectingAttribute(instance) {
104
+ instance[reflectingAttribute] = undefined;
105
+ }
106
+ function reflectPropertyToAttribute(instance, definition, value) {
107
+ const element = instance;
108
+ const attrName = definition.attribute;
109
+ if (definition.boolean) {
110
+ const shouldHaveAttribute = Boolean(value);
111
+ if (element.hasAttribute(attrName) === shouldHaveAttribute)
112
+ return;
113
+ setReflectingAttribute(instance, attrName);
114
+ try {
115
+ if (shouldHaveAttribute)
116
+ element.setAttribute(attrName, '');
117
+ else
118
+ element.removeAttribute(attrName);
119
+ }
120
+ finally {
121
+ restoreReflectingAttribute(instance);
122
+ }
123
+ return;
124
+ }
125
+ if (value == null) {
126
+ if (!element.hasAttribute(attrName))
127
+ return;
128
+ setReflectingAttribute(instance, attrName);
129
+ try {
130
+ element.removeAttribute(attrName);
131
+ }
132
+ finally {
133
+ restoreReflectingAttribute(instance);
134
+ }
135
+ return;
136
+ }
137
+ const attrValue = typeof value === 'string' ? value : String(value);
138
+ if (element.getAttribute(attrName) === attrValue)
139
+ return;
140
+ setReflectingAttribute(instance, attrName);
141
+ try {
142
+ element.setAttribute(attrName, attrValue);
143
+ }
144
+ finally {
145
+ restoreReflectingAttribute(instance);
146
+ }
147
+ }
148
+ export function syncAttrProperties(instance, ctor) {
149
+ const attrs = attrPropertyMapFor(ctor);
150
+ if (!attrs)
151
+ return;
152
+ const reactiveInstance = instance;
153
+ for (const definition of attrs.values()) {
154
+ reflectPropertyToAttribute(reactiveInstance, definition, reactiveInstance[definition.property]);
155
+ }
156
+ }
157
+ function createReactiveProperty(proto, name, attrDefinition) {
158
+ const backingKey = `_${name}`;
159
+ const changedKey = `${name}Changed`;
160
+ Object.defineProperty(proto, name, {
161
+ get() {
162
+ return this[backingKey];
163
+ },
164
+ set(newValue) {
165
+ const oldValue = this[backingKey];
166
+ if (Object.is(oldValue, newValue))
167
+ return;
168
+ this[backingKey] = newValue;
169
+ if (attrDefinition && this['$ready'] === true) {
170
+ reflectPropertyToAttribute(this, attrDefinition, newValue);
171
+ }
172
+ const cb = this[changedKey];
173
+ if (typeof cb === 'function') {
174
+ cb.call(this, oldValue, newValue);
175
+ }
176
+ if (this.isConnected) {
177
+ const upd = this['$update'];
178
+ if (upd)
179
+ upd.call(this, name);
180
+ }
181
+ },
182
+ enumerable: true,
183
+ configurable: true,
184
+ });
185
+ }
186
+ export function observable(target, name) {
187
+ const ctor = target.constructor;
188
+ registerObservableProperty(ctor, name);
189
+ createReactiveProperty(target, name);
190
+ }
249
191
  function applyAttr(target, name, options) {
250
192
  const proto = target;
251
193
  const ctor = proto.constructor;
@@ -255,11 +197,8 @@ function applyAttr(target, name, options) {
255
197
  property: name,
256
198
  boolean: options?.mode === 'boolean',
257
199
  };
258
- // 1. Install the reactive getter/setter (same as @observable), with
259
- // attribute reflection enabled after the element finishes hydration.
260
200
  registerObservableProperty(ctor, name);
261
201
  createReactiveProperty(proto, name, definition);
262
- // 2. Register the attribute mapping.
263
202
  let byAttribute = attrByAttribute.get(ctor);
264
203
  if (!byAttribute) {
265
204
  const inherited = inheritedAttrMap(attrByAttribute, ctor);
@@ -274,21 +213,17 @@ function applyAttr(target, name, options) {
274
213
  attrByProperty.set(ctor, byProperty);
275
214
  }
276
215
  byProperty.set(name, definition);
277
- // 3. Accumulate observed attributes on the constructor.
278
216
  if (!Object.prototype.hasOwnProperty.call(ctor, '_observedAttrs')) {
279
217
  const inheritedAttrs = ctor._observedAttrs;
280
218
  ctor._observedAttrs = inheritedAttrs ? inheritedAttrs.slice() : [];
281
- // Define the static getter that `customElements.define` inspects.
282
219
  Object.defineProperty(ctor, 'observedAttributes', {
283
220
  get() {
284
221
  return ctor._observedAttrs ?? [];
285
222
  },
286
223
  configurable: true,
287
224
  });
288
- // Patch `attributeChangedCallback` once per class.
289
225
  const origACB = proto['attributeChangedCallback'];
290
226
  proto['attributeChangedCallback'] = function (attribute, oldVal, newVal) {
291
- // Route the attribute change to the corresponding property.
292
227
  const definition = attrDefinitionFor(this.constructor, attribute);
293
228
  if (definition !== undefined &&
294
229
  this[reflectingAttribute] !== attribute) {
@@ -296,7 +231,6 @@ function applyAttr(target, name, options) {
296
231
  ? newVal !== null
297
232
  : newVal;
298
233
  }
299
- // Preserve any pre-existing attributeChangedCallback.
300
234
  if (origACB) {
301
235
  origACB.call(this, attribute, oldVal, newVal);
302
236
  }
@@ -304,15 +238,6 @@ function applyAttr(target, name, options) {
304
238
  }
305
239
  ctor._observedAttrs.push(attrName);
306
240
  }
307
- export function syncAttrProperties(instance, ctor) {
308
- const attrs = attrPropertyMapFor(ctor);
309
- if (!attrs)
310
- return;
311
- const reactiveInstance = instance;
312
- for (const definition of attrs.values()) {
313
- reflectPropertyToAttribute(reactiveInstance, definition, reactiveInstance[definition.property]);
314
- }
315
- }
316
241
  export function attr(targetOrOptions, name) {
317
242
  if (typeof name === 'string') {
318
243
  applyAttr(targetOrOptions, name);
@@ -1,23 +1,5 @@
1
- /**
2
- * Keyed child reconciliation for `@for(item of items)` repeat blocks.
3
- *
4
- * Diff that matches old instances by key, reuses what it
5
- * can, creates/removes the rest, then reorders DOM nodes in one forward pass.
6
- */
7
- import type { RepeatBinding, RepeatHost } from './types.js';
8
- /** Resolve a dotted path from a start offset without allocating. */
1
+ import type { RepeatBinding, RepeatHost, RepeatKeyState } from './types.js';
9
2
  export declare function dotWalk(cursor: unknown, path: string, from: number): unknown;
10
- /**
11
- * Resolve a dotted path against a repeat scope variable.
12
- *
13
- * When a binding inside `@for(item of items)` references `item.title`,
14
- * this function looks up `title` on the current scope value.
15
- */
16
- export declare function resolveRepeatValue(scopeVar: string, scope: unknown, path: string): unknown;
17
- /**
18
- * Reconcile a repeat binding against its current collection value.
19
- *
20
- * Called by `$updateInstance` on every reactive update. Resolves the
21
- * collection path, diffs old vs. new items by key, and patches the DOM.
22
- */
3
+ export declare function createRepeatKeyState(path: string): RepeatKeyState;
4
+ export declare function seedHydratedRepeatKeys(rep: RepeatBinding, items: unknown[]): void;
23
5
  export declare function syncRepeat(host: RepeatHost, rep: RepeatBinding): void;