@microsoft/webui-framework 0.0.13 → 0.0.15
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 +22 -13
- package/dist/element/diff.js +8 -6
- package/dist/element/styles.js +12 -11
- package/dist/element/types.d.ts +1 -0
- package/dist/element.d.ts +7 -0
- package/dist/element.js +146 -79
- package/dist/template-types.d.ts +4 -2
- package/dist/template.d.ts +2 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -95,6 +95,16 @@ cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
|
|
|
95
95
|
|
|
96
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
|
+
### Property binding lifecycle
|
|
99
|
+
|
|
100
|
+
Property bindings use the `:` prefix to pass values directly to child DOM properties:
|
|
101
|
+
|
|
102
|
+
```html
|
|
103
|
+
<profile-card :config="{{settings}}"></profile-card>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For client-created component trees, the runtime upgrades the cloned child elements while they are still detached, wires bindings, and applies the first binding pass before appending them to the connected DOM. A child can read an initial parent-provided property in `connectedCallback`. If the parent value is not set, the child may initialize its own fallback there, and later parent updates still flow through the live binding.
|
|
107
|
+
|
|
98
108
|
### DOM strategy (`--dom`)
|
|
99
109
|
|
|
100
110
|
The `--dom` flag controls how the server renders component content:
|
|
@@ -183,7 +193,7 @@ The WebUI plugin compiles these template features into runtime metadata:
|
|
|
183
193
|
|
|
184
194
|
- text bindings: `{{title}}`
|
|
185
195
|
- attribute bindings: `href="{{item.href}}"`
|
|
186
|
-
- event handlers: `@click="{onClick()}"`
|
|
196
|
+
- event handlers: `@click="{onClick()}"`, `@click="{onSelect(item.id, e)}"`
|
|
187
197
|
- refs: `w-ref="addInput"`
|
|
188
198
|
- conditionals: `<if condition="...">`
|
|
189
199
|
- repeats: `<for each="item in items">`
|
|
@@ -244,10 +254,10 @@ resource-constrained devices.
|
|
|
244
254
|
and cached as a `DocumentFragment`. Every subsequent instance uses
|
|
245
255
|
`cloneNode(true)` — DOM cloning is significantly faster than HTML parsing.
|
|
246
256
|
|
|
247
|
-
4. **
|
|
248
|
-
Event bindings
|
|
249
|
-
|
|
250
|
-
|
|
257
|
+
4. **Resolve event targets once.**
|
|
258
|
+
Event bindings store their target path in compiled metadata. Hydration
|
|
259
|
+
resolves each target once, installs the listener directly, and captures the
|
|
260
|
+
active repeat scope so handler arguments like `item.id` are read at dispatch.
|
|
251
261
|
|
|
252
262
|
5. **Single-pass hydration via path mapping.**
|
|
253
263
|
SSR DOM is matched to compiled template bindings through
|
|
@@ -285,7 +295,8 @@ When contributing to the runtime, avoid these patterns:
|
|
|
285
295
|
pre-resolved at hydration time via compiled path mapping.
|
|
286
296
|
- **Don't use recursion in hot paths.** Condition evaluation and DOM walks
|
|
287
297
|
use iterative stacks.
|
|
288
|
-
- **Don't
|
|
298
|
+
- **Don't allocate on the update path for events.** Event listeners are created
|
|
299
|
+
once during hydration and should not trigger extra DOM lookup work later.
|
|
289
300
|
- **Don't re-parse template HTML.** Always clone from the cached fragment.
|
|
290
301
|
|
|
291
302
|
---
|
|
@@ -447,12 +458,11 @@ interface TemplateMeta {
|
|
|
447
458
|
cl?: SlotPath[]; // Conditional anchor slots
|
|
448
459
|
r?: [collection, itemVar, blockIdx][];// Repeat blocks
|
|
449
460
|
rl?: SlotPath[]; // Repeat anchor slots
|
|
450
|
-
e?: [event, handler,
|
|
451
|
-
el?: NodePath[]; // Event target paths
|
|
461
|
+
e?: [event, handler, argSpecs, targetPath][]; // Events
|
|
452
462
|
b?: TemplateBlockMeta[]; // Nested block metadata
|
|
453
463
|
sa?: string; // Adopted stylesheet specifier
|
|
454
464
|
sd?: boolean; // Shadow DOM flag for client-created
|
|
455
|
-
re?: [event, handler,
|
|
465
|
+
re?: [event, handler, argSpecs][]; // Root-level events
|
|
456
466
|
}
|
|
457
467
|
```
|
|
458
468
|
|
|
@@ -461,7 +471,7 @@ interface TemplateMeta {
|
|
|
461
471
|
Template:
|
|
462
472
|
```html
|
|
463
473
|
<h1>{{title}}</h1>
|
|
464
|
-
<button @click="increment">Count: {{count}}</button>
|
|
474
|
+
<button @click="{increment()}">Count: {{count}}</button>
|
|
465
475
|
```
|
|
466
476
|
|
|
467
477
|
Compiled metadata:
|
|
@@ -472,8 +482,7 @@ Compiled metadata:
|
|
|
472
482
|
[[[0], 0], [["title"]]], // slot in <h1>, dynamic "title"
|
|
473
483
|
[[[1], 1], ["Count: ", ["count"]]] // slot in <button>, static + dynamic
|
|
474
484
|
],
|
|
475
|
-
e: [["click", "increment",
|
|
476
|
-
el: [[1]] // event target is child[1] (button)
|
|
485
|
+
e: [["click", "increment", [], [1]]] // click -> increment, no event args
|
|
477
486
|
}
|
|
478
487
|
```
|
|
479
488
|
|
|
@@ -624,7 +633,7 @@ The framework supports three CSS delivery strategies:
|
|
|
624
633
|
|----------|-------------|
|
|
625
634
|
| **Link** | `<link>` tag baked into `meta.h` — loaded by the browser naturally |
|
|
626
635
|
| **Inline** | `<style>` tag baked into `meta.h` — no external request |
|
|
627
|
-
| **Module** | `<
|
|
636
|
+
| **Module** | `<script type="importmap">{"imports":{"tag-name":"data:text/css,..."}}</script>` in the HTML payload registers the CSS as a module under `tag-name`. The framework imports it via `import(tag, { with: { type: 'css' } })` and applies the resulting `CSSStyleSheet` via `adoptedStyleSheets` for shadow DOM isolation |
|
|
628
637
|
|
|
629
638
|
CSS module stylesheets are cached so each component instance adopts the same
|
|
630
639
|
parsed sheet without re-parsing CSS. The `meta.sa` field specifies the
|
package/dist/element/diff.js
CHANGED
|
@@ -101,12 +101,11 @@ export function syncRepeat(host, rep) {
|
|
|
101
101
|
host.$removeInstance(oldInstances[i].instance);
|
|
102
102
|
}
|
|
103
103
|
rep.instances = next;
|
|
104
|
-
// Reorder + update
|
|
105
104
|
let cursor = rep.start;
|
|
106
105
|
for (let i = 0; i < next.length; i += 1) {
|
|
107
106
|
cursor = host.$insertInstanceAfter(cursor, container, next[i].instance);
|
|
108
107
|
}
|
|
109
|
-
for (let i = 0; i <
|
|
108
|
+
for (let i = 0; i < reuseCount; i += 1) {
|
|
110
109
|
host.$updateInstance(next[i].instance);
|
|
111
110
|
}
|
|
112
111
|
return;
|
|
@@ -148,13 +147,16 @@ export function syncRepeat(host, rep) {
|
|
|
148
147
|
}
|
|
149
148
|
rep.instances = next;
|
|
150
149
|
// ── Reorder DOM (forward pass) ──────────────────────────────────
|
|
151
|
-
//
|
|
150
|
+
// Newly-created instances were patched while detached. Reused instances
|
|
151
|
+
// update after moving so nested structural nodes stay with the item.
|
|
152
152
|
let cursor = rep.start;
|
|
153
153
|
for (let i = 0; i < next.length; i += 1) {
|
|
154
154
|
cursor = host.$insertInstanceAfter(cursor, container, next[i].instance);
|
|
155
155
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
for (let i = 0; i < oldInstances.length; i += 1) {
|
|
157
|
+
const entry = oldInstances[i];
|
|
158
|
+
const k = entry.key;
|
|
159
|
+
if (k != null && !oldByKey.has(k))
|
|
160
|
+
host.$updateInstance(entry.instance);
|
|
159
161
|
}
|
|
160
162
|
}
|
package/dist/element/styles.js
CHANGED
|
@@ -10,17 +10,17 @@
|
|
|
10
10
|
*
|
|
11
11
|
* - **Style**: Inline `<style>` tags inside each shadow template.
|
|
12
12
|
*
|
|
13
|
-
* - **Module**: Uses
|
|
14
|
-
* `<
|
|
15
|
-
* each rendered component's light DOM. The browser registers
|
|
16
|
-
* and automatically adopts
|
|
17
|
-
* declarative shadow roots.
|
|
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
18
|
*
|
|
19
|
-
* During SPA navigation, the router appends new
|
|
20
|
-
*
|
|
19
|
+
* During SPA navigation, the router appends new importmap script tags to
|
|
20
|
+
* `<head>` via `templateStyles[]`. The framework uses
|
|
21
21
|
* `import(specifier, { with: { type: "css" } })` to retrieve the browser's
|
|
22
22
|
* registered CSSStyleSheet and adopts it onto the shadow root. This is a
|
|
23
|
-
* direct hash-map lookup in the browser's module registry
|
|
23
|
+
* direct hash-map lookup in the browser's module registry - no DOM queries,
|
|
24
24
|
* no manual CSSStyleSheet construction.
|
|
25
25
|
*
|
|
26
26
|
* For light DOM components (no shadow root), Module mode injects a `<style>`
|
|
@@ -48,9 +48,10 @@ export function injectModuleStyle(specifier, shadowRoot) {
|
|
|
48
48
|
if (shadowRoot.adoptedStyleSheets.length > 0)
|
|
49
49
|
return;
|
|
50
50
|
// SPA path: import the CSS module from the browser's registry.
|
|
51
|
-
// The
|
|
52
|
-
//
|
|
53
|
-
// The import resolves to the same CSSStyleSheet the
|
|
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.
|
|
54
55
|
import(specifier, { with: { type: 'css' } }).then((mod) => {
|
|
55
56
|
shadowRoot.adoptedStyleSheets = [
|
|
56
57
|
...shadowRoot.adoptedStyleSheets,
|
package/dist/element/types.d.ts
CHANGED
|
@@ -98,6 +98,7 @@ export interface RepeatItemInstance {
|
|
|
98
98
|
*/
|
|
99
99
|
export interface RepeatHost {
|
|
100
100
|
$resolveValue(path: string, scope?: ScopeFrame): unknown;
|
|
101
|
+
/** Create, wire, and perform the first binding pass while detached. */
|
|
101
102
|
$createBlockInstance(blockIndex: number, scope?: ScopeFrame): TemplateInstance | null;
|
|
102
103
|
$updateInstance(instance: TemplateInstance): void;
|
|
103
104
|
$removeInstance(instance: TemplateInstance): void;
|
package/dist/element.d.ts
CHANGED
|
@@ -57,6 +57,9 @@ export declare class WebUIElement extends HTMLElement {
|
|
|
57
57
|
private $resolve;
|
|
58
58
|
private $resolveSSR;
|
|
59
59
|
private $parseTemplate;
|
|
60
|
+
private $createStagingRoot;
|
|
61
|
+
private $appendStagedChildren;
|
|
62
|
+
private $releaseStagingRepeatContainers;
|
|
60
63
|
private $wire;
|
|
61
64
|
/**
|
|
62
65
|
* Hydrate SSR-rendered DOM against compiled template metadata.
|
|
@@ -98,6 +101,8 @@ export declare class WebUIElement extends HTMLElement {
|
|
|
98
101
|
* `<!--wr-->...<!--/wr-->` ranges to keep text ordinals aligned.
|
|
99
102
|
*/
|
|
100
103
|
private $findSSRText;
|
|
104
|
+
/** Find the SSR insertion reference for an empty text slot. */
|
|
105
|
+
private $findSSRSlotRef;
|
|
101
106
|
/** Extract root tag name from block metadata. */
|
|
102
107
|
private $rootTag;
|
|
103
108
|
/** Wire attribute bindings using a resolver (shared by $wire and $hydrate). */
|
|
@@ -110,6 +115,8 @@ export declare class WebUIElement extends HTMLElement {
|
|
|
110
115
|
private $wireRoot;
|
|
111
116
|
/** Attach a single event listener. */
|
|
112
117
|
private $addEvent;
|
|
118
|
+
private $resolveEventArgs;
|
|
119
|
+
private $resolveEventArg;
|
|
113
120
|
/** Find w-ref attributes and assign to component properties. */
|
|
114
121
|
private $wireRefs;
|
|
115
122
|
/** Create an AttrBinding from compiled metadata. */
|
package/dist/element.js
CHANGED
|
@@ -157,6 +157,7 @@ export class WebUIElement extends HTMLElement {
|
|
|
157
157
|
const wantShadow = hasShadow || !!meta.sd;
|
|
158
158
|
let root;
|
|
159
159
|
let isSSR;
|
|
160
|
+
let clientRoot = null;
|
|
160
161
|
if (hasShadow) {
|
|
161
162
|
// Shadow DOM SSR — declarative shadow root already has content
|
|
162
163
|
root = this.shadowRoot;
|
|
@@ -175,14 +176,10 @@ export class WebUIElement extends HTMLElement {
|
|
|
175
176
|
// Existing children are slot content — they stay in light DOM
|
|
176
177
|
// and project through the template's <slot>.
|
|
177
178
|
root = this.attachShadow({ mode: 'open' });
|
|
178
|
-
const fragment = this.$parseTemplate(meta);
|
|
179
|
-
root.appendChild(fragment);
|
|
180
179
|
isSSR = false;
|
|
181
180
|
}
|
|
182
181
|
else {
|
|
183
182
|
// Light DOM client-created — populate from template (no shadow = no link issue)
|
|
184
|
-
const fragment = this.$parseTemplate(meta);
|
|
185
|
-
this.appendChild(fragment);
|
|
186
183
|
root = this;
|
|
187
184
|
isSSR = false;
|
|
188
185
|
}
|
|
@@ -196,7 +193,8 @@ export class WebUIElement extends HTMLElement {
|
|
|
196
193
|
this.$root = this.$hydrate(root, meta, getTemplateDom(meta));
|
|
197
194
|
}
|
|
198
195
|
else {
|
|
199
|
-
|
|
196
|
+
clientRoot = this.$createStagingRoot(meta);
|
|
197
|
+
this.$root = this.$wire(clientRoot, meta);
|
|
200
198
|
}
|
|
201
199
|
this.$meta = meta;
|
|
202
200
|
this.$hydrated = true;
|
|
@@ -206,8 +204,13 @@ export class WebUIElement extends HTMLElement {
|
|
|
206
204
|
// into the freshly-wired template DOM. Call $updateInstance directly
|
|
207
205
|
// to avoid the $update() path-index build — it will be lazy-built
|
|
208
206
|
// on the first reactive change instead.
|
|
209
|
-
if (!isSSR) {
|
|
207
|
+
if (!isSSR && clientRoot) {
|
|
210
208
|
this.$updateInstance(this.$root);
|
|
209
|
+
if (this.$root.repeats.length !== 0 || this.$root.conds.length !== 0) {
|
|
210
|
+
this.$root.nodes = childNodesArray(clientRoot);
|
|
211
|
+
this.$releaseStagingRepeatContainers(this.$root, clientRoot);
|
|
212
|
+
}
|
|
213
|
+
this.$appendStagedChildren(root, clientRoot);
|
|
211
214
|
}
|
|
212
215
|
hydrationEnd();
|
|
213
216
|
}
|
|
@@ -458,6 +461,52 @@ export class WebUIElement extends HTMLElement {
|
|
|
458
461
|
templateCache.set(meta, tpl.content);
|
|
459
462
|
return tpl.content.cloneNode(true);
|
|
460
463
|
}
|
|
464
|
+
$createStagingRoot(meta) {
|
|
465
|
+
const wrapper = document.createElement('div');
|
|
466
|
+
const fragment = this.$parseTemplate(meta);
|
|
467
|
+
wrapper.appendChild(fragment);
|
|
468
|
+
customElements.upgrade(wrapper);
|
|
469
|
+
return wrapper;
|
|
470
|
+
}
|
|
471
|
+
$appendStagedChildren(root, stagingRoot) {
|
|
472
|
+
const first = stagingRoot.firstChild;
|
|
473
|
+
if (!first)
|
|
474
|
+
return;
|
|
475
|
+
if (!first.nextSibling) {
|
|
476
|
+
root.appendChild(first);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const fragment = document.createDocumentFragment();
|
|
480
|
+
while (stagingRoot.firstChild) {
|
|
481
|
+
fragment.appendChild(stagingRoot.firstChild);
|
|
482
|
+
}
|
|
483
|
+
root.appendChild(fragment);
|
|
484
|
+
}
|
|
485
|
+
$releaseStagingRepeatContainers(instance, stagingRoot) {
|
|
486
|
+
if (!instance || !stagingRoot)
|
|
487
|
+
return;
|
|
488
|
+
if (instance.repeats.length === 0 && instance.conds.length === 0)
|
|
489
|
+
return;
|
|
490
|
+
const stack = [instance];
|
|
491
|
+
while (stack.length > 0) {
|
|
492
|
+
const current = stack.pop();
|
|
493
|
+
if (!current)
|
|
494
|
+
continue;
|
|
495
|
+
for (let i = 0; i < current.repeats.length; i++) {
|
|
496
|
+
const repeat = current.repeats[i];
|
|
497
|
+
if (repeat.container === stagingRoot)
|
|
498
|
+
repeat.container = null;
|
|
499
|
+
for (let j = 0; j < repeat.instances.length; j++) {
|
|
500
|
+
stack.push(repeat.instances[j].instance);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
for (let i = 0; i < current.conds.length; i++) {
|
|
504
|
+
const child = current.conds[i].instance;
|
|
505
|
+
if (child)
|
|
506
|
+
stack.push(child);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
461
510
|
// ═══════════════════════════════════════════════════════════════
|
|
462
511
|
// Client-created wiring — exact childNode index resolution
|
|
463
512
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -510,7 +559,7 @@ export class WebUIElement extends HTMLElement {
|
|
|
510
559
|
// Events + refs — resolve BEFORE anchors shift childNode indices.
|
|
511
560
|
// Events target element nodes (not text/comment positions), but anchor
|
|
512
561
|
// insertions still shift childNode indices for sibling elements.
|
|
513
|
-
this.$finalize(root, meta, (r, p) => this.$resolve(r, p));
|
|
562
|
+
this.$finalize(root, meta, (r, p) => this.$resolve(r, p), scope);
|
|
514
563
|
// Now insert anchors using pre-resolved references
|
|
515
564
|
// Text bindings
|
|
516
565
|
for (const t of textRefs) {
|
|
@@ -602,7 +651,12 @@ export class WebUIElement extends HTMLElement {
|
|
|
602
651
|
instance.texts.push({ node: textNode, parts, scope, raw: true, rawParent });
|
|
603
652
|
}
|
|
604
653
|
else {
|
|
605
|
-
|
|
654
|
+
let textNode = this.$findSSRText(ssrParent, tplParent, beforeIndex);
|
|
655
|
+
if (!textNode) {
|
|
656
|
+
textNode = document.createTextNode('');
|
|
657
|
+
const insertRef = this.$findSSRSlotRef(ssrParent, tplParent, beforeIndex);
|
|
658
|
+
ssrParent.insertBefore(textNode, insertRef);
|
|
659
|
+
}
|
|
606
660
|
if (textNode)
|
|
607
661
|
instance.texts.push({ node: textNode, parts, scope });
|
|
608
662
|
}
|
|
@@ -686,7 +740,11 @@ export class WebUIElement extends HTMLElement {
|
|
|
686
740
|
}
|
|
687
741
|
const blockMeta = this.$block(blockIndex);
|
|
688
742
|
const { attrMap, rootBindings } = this.$repeatMaps(blockIndex, itemVar);
|
|
689
|
-
const
|
|
743
|
+
const blockTplDom = blockMeta ? getTemplateDom(blockMeta) : null;
|
|
744
|
+
const rootTag = blockMeta && blockTplDom?.childNodes.length === 1 && blockTplDom.children.length === 1
|
|
745
|
+
? this.$rootTag(blockMeta)
|
|
746
|
+
: null;
|
|
747
|
+
const keyPath = Object.values(attrMap)[0];
|
|
690
748
|
// Find the next <!--wr--> marker in ssrParent (after any previously found one)
|
|
691
749
|
const marker = this.$findMarker(ssrParent, MARKER_REPEAT_START, lastRepMarker);
|
|
692
750
|
let anchor;
|
|
@@ -710,12 +768,11 @@ export class WebUIElement extends HTMLElement {
|
|
|
710
768
|
const { items: itemMarkers, end: endMarker } = marker
|
|
711
769
|
? collectItemMarkers(anchor)
|
|
712
770
|
: { items: [], end: null };
|
|
713
|
-
if (blockMeta && items.length > 0 && anchor.parentNode && itemMarkers.length > 0) {
|
|
771
|
+
if (blockMeta && blockTplDom && items.length > 0 && anchor.parentNode && itemMarkers.length > 0) {
|
|
714
772
|
if (itemMarkers.length !== items.length) {
|
|
715
773
|
console.warn(`[webui] hydration: repeat marker count (${itemMarkers.length}) ≠ data length (${items.length}) for "${collection}"`);
|
|
716
774
|
}
|
|
717
775
|
const firstKey = Object.keys(attrMap)[0];
|
|
718
|
-
const blockTplDom = getTemplateDom(blockMeta);
|
|
719
776
|
const limit = Math.min(itemMarkers.length, items.length);
|
|
720
777
|
for (let j = 0; j < limit; j++) {
|
|
721
778
|
const itemValue = items[j];
|
|
@@ -731,62 +788,25 @@ export class WebUIElement extends HTMLElement {
|
|
|
731
788
|
}
|
|
732
789
|
}
|
|
733
790
|
else {
|
|
734
|
-
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
if (blockMeta.c) {
|
|
743
|
-
// Walk between this <!--wi--> and the next boundary to find <!--wc--> markers
|
|
744
|
-
let cursor = itemMarkers[j].nextSibling;
|
|
745
|
-
const nextBound = j + 1 < itemMarkers.length ? itemMarkers[j + 1] : endMarker;
|
|
746
|
-
const itemParent = itemMarkers[j].parentNode;
|
|
747
|
-
for (let ci = 0; ci < blockMeta.c.length; ci++) {
|
|
748
|
-
const [condCond, condBlockIndex] = blockMeta.c[ci];
|
|
749
|
-
// Find <!--wc--> within this item's range
|
|
750
|
-
let condAnchor = null;
|
|
751
|
-
while (cursor && cursor !== nextBound) {
|
|
752
|
-
if (cursor.nodeType === 8 && cursor.data === MARKER_COND_START) {
|
|
753
|
-
condAnchor = cursor;
|
|
754
|
-
cursor = cursor.nextSibling;
|
|
755
|
-
break;
|
|
756
|
-
}
|
|
757
|
-
cursor = cursor.nextSibling;
|
|
758
|
-
}
|
|
759
|
-
if (!condAnchor) {
|
|
760
|
-
condAnchor = document.createComment('');
|
|
761
|
-
if (itemParent)
|
|
762
|
-
itemParent.insertBefore(condAnchor, cursor ?? null);
|
|
763
|
-
}
|
|
764
|
-
const condMet = condCond[0](this.$resolver, itemScope);
|
|
765
|
-
let condInstance = null;
|
|
766
|
-
if (condMet) {
|
|
767
|
-
const condBlockMeta = this.$block(condBlockIndex);
|
|
768
|
-
if (condBlockMeta) {
|
|
769
|
-
condInstance = this.$hydrateCondContent(condAnchor, condBlockMeta, itemScope);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
// Remove <!--/wc--> end marker and advance cursor past it
|
|
773
|
-
const lastNode = condInstance ? condInstance.nodes[condInstance.nodes.length - 1] : condAnchor;
|
|
774
|
-
const endM = lastNode?.nextSibling;
|
|
775
|
-
if (endM && endM.nodeType === 8 && endM.data === MARKER_COND_END) {
|
|
776
|
-
cursor = endM.nextSibling;
|
|
777
|
-
endM.parentNode?.removeChild(endM);
|
|
778
|
-
}
|
|
779
|
-
else {
|
|
780
|
-
cursor = lastNode?.nextSibling ?? null;
|
|
781
|
-
}
|
|
782
|
-
inst.conds.push({
|
|
783
|
-
condition: condCond, blockIndex: condBlockIndex,
|
|
784
|
-
anchor: condAnchor, scope: itemScope,
|
|
785
|
-
instance: condInstance,
|
|
786
|
-
});
|
|
787
|
-
}
|
|
791
|
+
const itemParent = itemMarkers[j].parentNode;
|
|
792
|
+
const nextBound = j + 1 < itemMarkers.length ? itemMarkers[j + 1] : endMarker;
|
|
793
|
+
const wrapper = document.createElement('div');
|
|
794
|
+
let cursor = itemMarkers[j].nextSibling;
|
|
795
|
+
while (cursor && cursor !== nextBound) {
|
|
796
|
+
const next = cursor.nextSibling;
|
|
797
|
+
wrapper.appendChild(cursor);
|
|
798
|
+
cursor = next;
|
|
788
799
|
}
|
|
789
|
-
|
|
800
|
+
const inst = this.$hydrate(wrapper, blockMeta, blockTplDom, itemScope);
|
|
801
|
+
inst.nodes = childNodesArray(wrapper);
|
|
802
|
+
let afterNode = itemMarkers[j];
|
|
803
|
+
for (let nodeIndex = 0; nodeIndex < inst.nodes.length; nodeIndex++) {
|
|
804
|
+
const node = inst.nodes[nodeIndex];
|
|
805
|
+
itemParent?.insertBefore(node, afterNode.nextSibling);
|
|
806
|
+
afterNode = node;
|
|
807
|
+
}
|
|
808
|
+
const key = keyPath ? String(dotWalk(itemValue, keyPath, 0) ?? '') : null;
|
|
809
|
+
repeatInsts.push({ key, value: itemValue, instance: inst });
|
|
790
810
|
}
|
|
791
811
|
}
|
|
792
812
|
// Defer <!--wi--> item marker removal (anchor <!--wr--> stays
|
|
@@ -808,7 +828,7 @@ export class WebUIElement extends HTMLElement {
|
|
|
808
828
|
}
|
|
809
829
|
}
|
|
810
830
|
// Events + refs — this is the last phase that uses $resolveSSR.
|
|
811
|
-
this.$finalize(ssrRoot, meta, (r, p) => this.$resolveSSR(r, tplDom, p, pathStart));
|
|
831
|
+
this.$finalize(ssrRoot, meta, (r, p) => this.$resolveSSR(r, tplDom, p, pathStart), scope);
|
|
812
832
|
// All path-based resolution is complete. Remove the SSR markers that
|
|
813
833
|
// were kept alive for structural-block skipping. Start markers
|
|
814
834
|
// (<!--wc-->, <!--wr-->) are intentionally NOT collected — they
|
|
@@ -931,6 +951,18 @@ export class WebUIElement extends HTMLElement {
|
|
|
931
951
|
}
|
|
932
952
|
return null;
|
|
933
953
|
}
|
|
954
|
+
/** Find the SSR insertion reference for an empty text slot. */
|
|
955
|
+
$findSSRSlotRef(ssrParent, tplParent, beforeIndex) {
|
|
956
|
+
const ordinals = getTplOrdinals(tplParent);
|
|
957
|
+
const children = tplParent.childNodes;
|
|
958
|
+
for (let i = beforeIndex; i < children.length; i++) {
|
|
959
|
+
const entry = ordinals.get(i);
|
|
960
|
+
if (!entry)
|
|
961
|
+
continue;
|
|
962
|
+
return findByOrdinal(ssrParent, entry[0], entry[1]);
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
934
966
|
/** Extract root tag name from block metadata. */
|
|
935
967
|
$rootTag(meta) {
|
|
936
968
|
let cached = rootTagCache.get(meta);
|
|
@@ -972,37 +1004,68 @@ export class WebUIElement extends HTMLElement {
|
|
|
972
1004
|
}
|
|
973
1005
|
}
|
|
974
1006
|
/** Wire events + root events + refs (shared by $wire and $hydrate). */
|
|
975
|
-
$finalize(root, meta, resolver) {
|
|
976
|
-
this.$wireEvents(root, meta, resolver);
|
|
1007
|
+
$finalize(root, meta, resolver, scope) {
|
|
1008
|
+
this.$wireEvents(root, meta, resolver, scope);
|
|
977
1009
|
if (meta.re)
|
|
978
1010
|
this.$wireRoot(meta.re);
|
|
979
1011
|
this.$wireRefs(root);
|
|
980
1012
|
}
|
|
981
1013
|
/** Wire events using a resolver function (works for both client and SSR). */
|
|
982
|
-
$wireEvents(root, meta, resolver) {
|
|
1014
|
+
$wireEvents(root, meta, resolver, scope) {
|
|
983
1015
|
if (!meta.e)
|
|
984
1016
|
return;
|
|
985
1017
|
for (let i = 0; i < meta.e.length; i++) {
|
|
986
|
-
const [eventName, handlerName,
|
|
1018
|
+
const [eventName, handlerName, args, target] = meta.e[i];
|
|
987
1019
|
const el = resolver(root, target);
|
|
988
1020
|
if (!el || el.nodeType !== 1)
|
|
989
1021
|
continue;
|
|
990
|
-
this.$addEvent(el, eventName, handlerName,
|
|
1022
|
+
this.$addEvent(el, eventName, handlerName, args, scope);
|
|
991
1023
|
}
|
|
992
1024
|
}
|
|
993
1025
|
/** Wire root-level events on the host element (or shadow root when present). */
|
|
994
1026
|
$wireRoot(re) {
|
|
995
1027
|
const target = this.shadowRoot ?? this;
|
|
996
1028
|
for (let i = 0; i < re.length; i++) {
|
|
997
|
-
this.$addEvent(target, re[i][0], re[i][1], re[i][2]);
|
|
1029
|
+
this.$addEvent(target, re[i][0], re[i][1], re[i][2], undefined);
|
|
998
1030
|
}
|
|
999
1031
|
}
|
|
1000
1032
|
/** Attach a single event listener. */
|
|
1001
|
-
$addEvent(target, eventName, handlerName,
|
|
1033
|
+
$addEvent(target, eventName, handlerName, args, scope) {
|
|
1002
1034
|
const method = this[handlerName];
|
|
1003
1035
|
if (typeof method !== 'function')
|
|
1004
1036
|
return;
|
|
1005
|
-
|
|
1037
|
+
if (args.length === 0) {
|
|
1038
|
+
target.addEventListener(eventName, () => {
|
|
1039
|
+
method.call(this);
|
|
1040
|
+
});
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (args.length === 1 && args[0][0] === 'e') {
|
|
1044
|
+
target.addEventListener(eventName, (event) => {
|
|
1045
|
+
method.call(this, event);
|
|
1046
|
+
});
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
target.addEventListener(eventName, (event) => {
|
|
1050
|
+
method.apply(this, this.$resolveEventArgs(args, event, scope));
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
$resolveEventArgs(args, event, scope) {
|
|
1054
|
+
const resolved = [];
|
|
1055
|
+
for (let i = 0; i < args.length; i++) {
|
|
1056
|
+
resolved.push(this.$resolveEventArg(args[i], event, scope));
|
|
1057
|
+
}
|
|
1058
|
+
return resolved;
|
|
1059
|
+
}
|
|
1060
|
+
$resolveEventArg(arg, event, scope) {
|
|
1061
|
+
switch (arg[0]) {
|
|
1062
|
+
case 'e': return event;
|
|
1063
|
+
case 'p': return this.$resolveValue(arg[1], scope);
|
|
1064
|
+
case 's': return arg[1];
|
|
1065
|
+
case 'n': return arg[1];
|
|
1066
|
+
case 'b': return !!arg[1];
|
|
1067
|
+
case 'z': return null;
|
|
1068
|
+
}
|
|
1006
1069
|
}
|
|
1007
1070
|
/** Find w-ref attributes and assign to component properties. */
|
|
1008
1071
|
$wireRefs(root) {
|
|
@@ -1253,8 +1316,9 @@ export class WebUIElement extends HTMLElement {
|
|
|
1253
1316
|
c.anchor.parentNode?.insertBefore(frag, c.anchor.nextSibling);
|
|
1254
1317
|
}
|
|
1255
1318
|
}
|
|
1256
|
-
|
|
1319
|
+
else {
|
|
1257
1320
|
this.$updateInstance(c.instance);
|
|
1321
|
+
}
|
|
1258
1322
|
}
|
|
1259
1323
|
else if (c.instance) {
|
|
1260
1324
|
this.$removeInstance(c.instance);
|
|
@@ -1300,11 +1364,14 @@ export class WebUIElement extends HTMLElement {
|
|
|
1300
1364
|
const bm = this.$block(blockIndex);
|
|
1301
1365
|
if (!bm)
|
|
1302
1366
|
return null;
|
|
1303
|
-
const
|
|
1304
|
-
const wrapper = document.createElement('div');
|
|
1305
|
-
wrapper.appendChild(frag);
|
|
1367
|
+
const wrapper = this.$createStagingRoot(bm);
|
|
1306
1368
|
const inst = this.$wire(wrapper, bm, scope);
|
|
1307
1369
|
inst.nodes = childNodesArray(wrapper);
|
|
1370
|
+
this.$updateInstance(inst);
|
|
1371
|
+
if (inst.repeats.length !== 0 || inst.conds.length !== 0) {
|
|
1372
|
+
inst.nodes = childNodesArray(wrapper);
|
|
1373
|
+
this.$releaseStagingRepeatContainers(inst, wrapper);
|
|
1374
|
+
}
|
|
1308
1375
|
return inst;
|
|
1309
1376
|
}
|
|
1310
1377
|
$removeInstance(instance) {
|
package/dist/template-types.d.ts
CHANGED
|
@@ -30,7 +30,9 @@ export type CompiledCondition = [
|
|
|
30
30
|
export type CompiledConditionalMeta = [condition: CompiledCondition, blockIndex: number, slot: TemplateSlotPath];
|
|
31
31
|
export type CompiledAttrMeta = [name: string, kind: 0, value: string] | [name: string, kind: 1, value: string] | [name: string, kind: 2, condition: CompiledCondition] | [name: string, kind: 3, parts: CompiledAttrPart[]];
|
|
32
32
|
export type CompiledRepeatMeta = [collection: string, itemVar: string, blockIndex: number, slot: TemplateSlotPath];
|
|
33
|
-
export type
|
|
33
|
+
export type CompiledEventArg = ['e'] | ['p', string] | ['s', string] | ['n', number] | ['b', number] | ['z'];
|
|
34
|
+
export type CompiledEventArgs = CompiledEventArg[];
|
|
35
|
+
export type CompiledEventMeta = [name: string, handler: string, args: CompiledEventArgs, target: TemplateNodePath];
|
|
34
36
|
export interface TemplateBlockMeta {
|
|
35
37
|
h: string;
|
|
36
38
|
tx?: CompiledTextRunMeta[];
|
|
@@ -43,7 +45,7 @@ export interface TemplateBlockMeta {
|
|
|
43
45
|
export interface TemplateMeta extends TemplateBlockMeta {
|
|
44
46
|
b?: TemplateBlockMeta[];
|
|
45
47
|
sa?: string;
|
|
46
|
-
re?: [string, string,
|
|
48
|
+
re?: [string, string, CompiledEventArgs][];
|
|
47
49
|
/** Shadow DOM flag — when true, client-created components use shadow root. */
|
|
48
50
|
sd?: boolean;
|
|
49
51
|
}
|
package/dist/template.d.ts
CHANGED
|
@@ -10,14 +10,13 @@
|
|
|
10
10
|
* - `cl` — conditional anchor slots
|
|
11
11
|
* - `r` — repeat/for blocks `[collection, itemVar, blockIndex]`
|
|
12
12
|
* - `rl` — repeat anchor slots
|
|
13
|
-
* - `e` — element events `[eventName, handlerName,
|
|
14
|
-
* - `el` — event target element paths
|
|
13
|
+
* - `e` — element events `[eventName, handlerName, argSpecs, targetPath]`
|
|
15
14
|
* - `b` — nested compiled block metadata
|
|
16
15
|
* - `sa` — adopted stylesheet specifier for CSS module strategy
|
|
17
16
|
* - `sd` — shadow DOM flag for client-created components
|
|
18
17
|
* - `re` — root events on the host element
|
|
19
18
|
*/
|
|
20
|
-
export type { CompiledAttrGroupMeta, CompiledAttrMeta, CompiledAttrPart, CompiledCondition, CompiledConditionalMeta, CompiledTextRunMeta, TemplateBlockMeta, TemplateMeta, TemplateNodePath, TemplateSlotPath, } from './template-types.js';
|
|
19
|
+
export type { CompiledAttrGroupMeta, CompiledAttrMeta, CompiledAttrPart, CompiledCondition, CompiledConditionalMeta, CompiledEventArg, CompiledEventArgs, CompiledTextRunMeta, TemplateBlockMeta, TemplateMeta, TemplateNodePath, TemplateSlotPath, } from './template-types.js';
|
|
21
20
|
import type { TemplateMeta } from './template-types.js';
|
|
22
21
|
declare global {
|
|
23
22
|
interface Window {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/webui-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
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.15"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"build": "tsc",
|