@microsoft/webui-framework 0.0.8 → 0.0.10
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/dist/decorators.d.ts +30 -0
- package/dist/decorators.js +74 -3
- package/dist/element/diff.js +1 -1
- package/dist/element.js +16 -9
- package/package.json +2 -2
package/dist/decorators.d.ts
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
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
|
+
export declare function toKebabCase(str: string): string;
|
|
1
31
|
export declare function getObservableNames(ctor: Function): Set<string>;
|
|
2
32
|
/**
|
|
3
33
|
* Marks a property as observable. When the value changes the decorator will:
|
package/dist/decorators.js
CHANGED
|
@@ -9,9 +9,80 @@
|
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
10
|
// Internal helpers
|
|
11
11
|
// ---------------------------------------------------------------------------
|
|
12
|
-
/**
|
|
13
|
-
|
|
14
|
-
|
|
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
|
+
const propertyToAttribute = Object.assign(Object.create(null), {
|
|
20
|
+
// --- HTML global/element attributes ---
|
|
21
|
+
accessKey: 'accesskey',
|
|
22
|
+
autoCapitalize: 'autocapitalize',
|
|
23
|
+
contentEditable: 'contenteditable',
|
|
24
|
+
crossOrigin: 'crossorigin',
|
|
25
|
+
dirName: 'dirname',
|
|
26
|
+
fetchPriority: 'fetchpriority',
|
|
27
|
+
formAction: 'formaction',
|
|
28
|
+
formEnctype: 'formenctype',
|
|
29
|
+
formMethod: 'formmethod',
|
|
30
|
+
formNoValidate: 'formnovalidate',
|
|
31
|
+
formTarget: 'formtarget',
|
|
32
|
+
inputMode: 'inputmode',
|
|
33
|
+
isMap: 'ismap',
|
|
34
|
+
maxLength: 'maxlength',
|
|
35
|
+
minLength: 'minlength',
|
|
36
|
+
noModule: 'nomodule',
|
|
37
|
+
noValidate: 'novalidate',
|
|
38
|
+
readOnly: 'readonly',
|
|
39
|
+
referrerPolicy: 'referrerpolicy',
|
|
40
|
+
tabIndex: 'tabindex',
|
|
41
|
+
useMap: 'usemap',
|
|
42
|
+
});
|
|
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
|
+
export function toKebabCase(str) {
|
|
73
|
+
const mapped = propertyToAttribute[str];
|
|
74
|
+
if (mapped)
|
|
75
|
+
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) {
|
|
78
|
+
return 'aria-' + str.slice(4).toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
let out = '';
|
|
81
|
+
for (let i = 0; i < str.length; i++) {
|
|
82
|
+
const code = str.charCodeAt(i);
|
|
83
|
+
out += code >= 65 && code <= 90 ? '-' + String.fromCharCode(code + 32) : str[i];
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
15
86
|
}
|
|
16
87
|
/**
|
|
17
88
|
* Shared logic for installing a reactive getter/setter on a class prototype.
|
package/dist/element/diff.js
CHANGED
|
@@ -28,7 +28,7 @@ export function dotWalk(cursor, path, from) {
|
|
|
28
28
|
export function resolveRepeatValue(scopeVar, scope, path) {
|
|
29
29
|
if (path === scopeVar)
|
|
30
30
|
return scope;
|
|
31
|
-
if (!path.startsWith(
|
|
31
|
+
if (path.length <= scopeVar.length || path.charCodeAt(scopeVar.length) !== 46 /* '.' */ || !path.startsWith(scopeVar))
|
|
32
32
|
return undefined;
|
|
33
33
|
return dotWalk(scope, path, scopeVar.length + 1);
|
|
34
34
|
}
|
package/dist/element.js
CHANGED
|
@@ -48,8 +48,8 @@ import { ATTR_KIND_BOOLEAN, ATTR_KIND_COMPLEX, ATTR_KIND_TEMPLATE, } from './ele
|
|
|
48
48
|
// ── Caches ──────────────────────────────────────────────────────
|
|
49
49
|
/** Parsed template cache — cloneNode(true) is faster than re-parsing. */
|
|
50
50
|
const templateCache = new WeakMap();
|
|
51
|
-
/** Parsed template DOM for SSR path mapping, keyed by
|
|
52
|
-
const templateDOMCache = new
|
|
51
|
+
/** Parsed template DOM for SSR path mapping, keyed by TemplateBlockMeta. */
|
|
52
|
+
const templateDOMCache = new WeakMap();
|
|
53
53
|
/** Pre-computed ordinals for template nodes: childIndex → [nodeType, ordinal].
|
|
54
54
|
* Avoids re-counting element/text siblings on every $resolveSSR call. */
|
|
55
55
|
const tplOrdinalCache = new WeakMap();
|
|
@@ -88,12 +88,12 @@ function childNodesArray(parent) {
|
|
|
88
88
|
}
|
|
89
89
|
// ── Helper: parse template HTML into a temp container ────────────
|
|
90
90
|
function getTemplateDom(meta) {
|
|
91
|
-
let cached = templateDOMCache.get(meta
|
|
91
|
+
let cached = templateDOMCache.get(meta);
|
|
92
92
|
if (cached)
|
|
93
93
|
return cached;
|
|
94
94
|
const div = document.createElement('div');
|
|
95
95
|
div.innerHTML = meta.h;
|
|
96
|
-
templateDOMCache.set(meta
|
|
96
|
+
templateDOMCache.set(meta, div);
|
|
97
97
|
return div;
|
|
98
98
|
}
|
|
99
99
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -208,7 +208,12 @@ export class WebUIElement extends HTMLElement {
|
|
|
208
208
|
}
|
|
209
209
|
hydrationEnd();
|
|
210
210
|
}
|
|
211
|
-
disconnectedCallback() {
|
|
211
|
+
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.
|
|
216
|
+
}
|
|
212
217
|
/** Dispatch a bubbling custom event. Uses composed:true when in shadow DOM. */
|
|
213
218
|
$emit(name, detail) {
|
|
214
219
|
return this.dispatchEvent(new CustomEvent(name, {
|
|
@@ -775,23 +780,25 @@ export class WebUIElement extends HTMLElement {
|
|
|
775
780
|
*/
|
|
776
781
|
$hydrateCondContent(condAnchor, blockMeta, scope) {
|
|
777
782
|
const rootTag = this.$rootTag(blockMeta);
|
|
778
|
-
|
|
783
|
+
const tplDom = getTemplateDom(blockMeta);
|
|
784
|
+
if (rootTag && tplDom.children.length === 1) {
|
|
785
|
+
// Single-root optimisation: hydrate the element in-place (pathStart=1).
|
|
779
786
|
const el = nextElement(condAnchor);
|
|
780
787
|
if (el) {
|
|
781
|
-
const inst = this.$hydrate(el, blockMeta,
|
|
788
|
+
const inst = this.$hydrate(el, blockMeta, tplDom, scope, 1);
|
|
782
789
|
this.$updateInstance(inst);
|
|
783
790
|
return inst;
|
|
784
791
|
}
|
|
785
792
|
return null;
|
|
786
793
|
}
|
|
787
|
-
//
|
|
794
|
+
// Multi-root or text-only conditional: collect nodes between <!--wc--> and <!--/wc-->
|
|
788
795
|
const condNodes = this.$collectBetween(condAnchor, MARKER_COND_END);
|
|
789
796
|
if (condNodes.length === 0)
|
|
790
797
|
return null;
|
|
791
798
|
const wrapper = document.createElement('div');
|
|
792
799
|
for (let cn = 0; cn < condNodes.length; cn++)
|
|
793
800
|
wrapper.appendChild(condNodes[cn]);
|
|
794
|
-
const inst = this.$hydrate(wrapper, blockMeta,
|
|
801
|
+
const inst = this.$hydrate(wrapper, blockMeta, tplDom, scope);
|
|
795
802
|
inst.nodes = childNodesArray(wrapper);
|
|
796
803
|
let afterNode = condAnchor;
|
|
797
804
|
for (let cn = 0; cn < inst.nodes.length; cn++) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/webui-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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.10"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|