@microsoft/webui-framework 0.0.18 → 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.
- package/README.md +75 -60
- package/dist/decorators.d.ts +1 -0
- package/dist/decorators.js +3 -0
- package/dist/element/diff.d.ts +3 -1
- package/dist/element/diff.js +197 -90
- package/dist/element/markers.js +13 -5
- package/dist/element/types.d.ts +18 -10
- package/dist/element/types.js +3 -0
- package/dist/element.js +3 -2
- package/dist/hydration-mismatch.js +3 -2
- package/dist/index.js +1 -1
- package/dist/static-host.js +16 -8
- package/dist/template-element.d.ts +20 -9
- package/dist/template-element.js +345 -205
- package/dist/template-events.js +1 -1
- package/dist/template-roots.d.ts +0 -1
- package/dist/template-roots.js +0 -11
- package/dist/template-types.d.ts +7 -1
- package/dist/template.d.ts +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -87,16 +87,21 @@ Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--
|
|
|
87
87
|
<counter-card label="Taps"></counter-card>
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
### HTML-only components
|
|
90
|
+
### HTML-only dormant components
|
|
91
91
|
|
|
92
92
|
If a component has no event handlers, custom lifecycle code, or client-only
|
|
93
93
|
methods, it can ship only `component.html` and optional `component.css`.
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
The sibling `.ts` or `.js` file is the authored behavior boundary. With
|
|
96
|
+
manifest-enabled projection, only `@observable` and `@attr` fields opt into
|
|
97
|
+
initial state hydration; template-only roots stay in the trusted SSR DOM.
|
|
98
|
+
Without a module, template bindings render on the server and the component
|
|
99
|
+
contributes no projected keys. Without projection metadata, the server
|
|
100
|
+
preserves full state. The compiler still emits template metadata for scriptless
|
|
101
|
+
components. When the framework is loaded, it can activate that template when
|
|
102
|
+
browser state or client-side creation needs it.
|
|
103
|
+
If that first write omits a repeat collection, the host preserves the existing
|
|
104
|
+
SSR items until the collection is explicitly supplied.
|
|
100
105
|
|
|
101
106
|
Create a custom element only for an Interactive Island: event handlers, custom
|
|
102
107
|
lifecycle code, imperative methods, or state that TypeScript code reads or
|
|
@@ -111,9 +116,15 @@ cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
|
|
|
111
116
|
|
|
112
117
|
The WebUI plugin prepares component templates for the browser. Bundle your
|
|
113
118
|
source browser entry directly. Import `@microsoft/webui-framework` from authored
|
|
114
|
-
component modules
|
|
115
|
-
|
|
116
|
-
state.
|
|
119
|
+
component modules. An app that stays static after SSR needs no framework
|
|
120
|
+
browser import. Import the framework once when HTML-only components must accept
|
|
121
|
+
browser state or participate in soft navigation.
|
|
122
|
+
|
|
123
|
+
The plugin alone preserves full server state. To emit exact `@observable` and
|
|
124
|
+
`@attr` state surfaces, run the application's bundler first with
|
|
125
|
+
`@microsoft/webui/projection.js`, then pass its manifest to `webui build` with
|
|
126
|
+
`--projection-manifest`. The manifest tooling is build-only; this runtime
|
|
127
|
+
package does not depend on esbuild or TypeScript.
|
|
117
128
|
|
|
118
129
|
### Property binding lifecycle
|
|
119
130
|
|
|
@@ -140,6 +151,13 @@ development-only and is dead-code-eliminated from production bundles via the
|
|
|
140
151
|
`false`). See the
|
|
141
152
|
[Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
|
|
142
153
|
|
|
154
|
+
`super.connectedCallback()` is the synchronous hydration boundary for an
|
|
155
|
+
authored component. When it returns, bindings, events, and `w-ref` references
|
|
156
|
+
are wired. Use a parser-inserted, non-async ES module script or a classic
|
|
157
|
+
`defer` script. A blocking classic script must follow every SSR instance it may
|
|
158
|
+
upgrade. Descendants must not structurally mutate a containing component's SSR
|
|
159
|
+
subtree before it hydrates, because hydration relies on stable compiled paths.
|
|
160
|
+
|
|
143
161
|
### DOM strategy (`--dom`)
|
|
144
162
|
|
|
145
163
|
The `--dom` flag controls how the server renders component content:
|
|
@@ -240,6 +258,7 @@ Notes:
|
|
|
240
258
|
|
|
241
259
|
- default attribute names use kebab-case
|
|
242
260
|
- attribute values arrive as strings
|
|
261
|
+
- during SSR hydration, an existing host attribute wins over projected state
|
|
243
262
|
- use `@observable` for state that client code reads or mutates
|
|
244
263
|
|
|
245
264
|
### `@volatile`
|
|
@@ -269,7 +288,8 @@ The WebUI plugin supports these template features:
|
|
|
269
288
|
- repeats: `<for each="item in items">`
|
|
270
289
|
|
|
271
290
|
Components that use `@event` must have authored `.ts` or `.js` code that
|
|
272
|
-
defines a `WebUIElement` for the tag
|
|
291
|
+
defines a `WebUIElement` for the tag. HTML-only components do not provide
|
|
292
|
+
application event handlers.
|
|
273
293
|
|
|
274
294
|
Example from `examples/app/todo-webui`:
|
|
275
295
|
|
|
@@ -302,8 +322,10 @@ Root-level events (e.g. `@toggle-item="{onToggleItem(e)}"`) can be declared on t
|
|
|
302
322
|
- Use `w-ref` for true DOM-only concerns like focus or reading input values.
|
|
303
323
|
- Omit `@observable` for values that are only read by the template and seeded
|
|
304
324
|
externally after construction.
|
|
305
|
-
- Omit the TypeScript class
|
|
306
|
-
|
|
325
|
+
- Omit the TypeScript class when compiled template behavior is sufficient,
|
|
326
|
+
including browser-applied state, route updates, and client-created instances.
|
|
327
|
+
Add a same-named module only for authored events, lifecycle, decorators, or
|
|
328
|
+
imperative APIs.
|
|
307
329
|
|
|
308
330
|
Avoid imperative DOM mutation for application state that can be represented by reactive properties.
|
|
309
331
|
|
|
@@ -437,7 +459,7 @@ flowchart LR
|
|
|
437
459
|
graph TD
|
|
438
460
|
EL["element.ts (~850 lines)<br/><i>Orchestrator</i><br/>$mount, $wire, $hydrate,<br/>$resolveSSR, $applySSRState,<br/>$update, events, cleanup"]
|
|
439
461
|
|
|
440
|
-
DIFF["element/diff.ts
|
|
462
|
+
DIFF["element/diff.ts<br/><i>List Reconciliation</i><br/>positional + explicit-key diffing<br/>for <for> repeat blocks"]
|
|
441
463
|
|
|
442
464
|
COND["element/conditions.ts<br/><i>Condition Evaluation</i><br/>evaluateCondition (iterative),<br/>conditionUsesPath"]
|
|
443
465
|
|
|
@@ -484,7 +506,7 @@ sequenceDiagram
|
|
|
484
506
|
CE->>CE: attributeChangedCallback (pre-existing attrs)
|
|
485
507
|
CE->>FW: connectedCallback() → $mount()
|
|
486
508
|
FW->>FW: SSR DOM detected (shadow root or children exist)
|
|
487
|
-
FW->>FW: $applySSRState() — seed decorated
|
|
509
|
+
FW->>FW: $applySSRState() — seed decorated state
|
|
488
510
|
FW->>FW: $hydrate() — template-parallel path resolution
|
|
489
511
|
FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
|
|
490
512
|
FW->>FW: $wireEvents() + $wireRefs()
|
|
@@ -538,7 +560,8 @@ interface TemplateMeta {
|
|
|
538
560
|
sa?: string; // Adopted stylesheet specifier
|
|
539
561
|
sd?: boolean; // Shadow DOM flag for client-created
|
|
540
562
|
re?: [event, handler, argSpecs][]; // Root-level events
|
|
541
|
-
|
|
563
|
+
tr?: string[]; // Template state roots
|
|
564
|
+
ta?: string[]; // Host attributes aligned with tr
|
|
542
565
|
}
|
|
543
566
|
```
|
|
544
567
|
|
|
@@ -635,61 +658,53 @@ sees `42` in the DOM before the component's JavaScript state exists. Without
|
|
|
635
658
|
seeding, the first `$update()` would overwrite the SSR content with the wrong
|
|
636
659
|
value.
|
|
637
660
|
|
|
638
|
-
State seeding uses `window.__webui.state`
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
661
|
+
State seeding uses `window.__webui.state` loaded from the server-emitted
|
|
662
|
+
`#webui-data` block. When the protocol contains projection metadata, only
|
|
663
|
+
`@observable` and `@attr` keys from reachable authored components select
|
|
664
|
+
initial state; HTML-only dormant components and authored template-only roots
|
|
665
|
+
contribute no startup keys. Without projection metadata, the server preserves
|
|
666
|
+
full state. During `$mount()`, `$applySSRState()` writes matching decorated keys
|
|
667
|
+
directly to observable backing fields before any bindings are wired:
|
|
644
668
|
|
|
645
669
|
```mermaid
|
|
646
670
|
flowchart LR
|
|
647
|
-
SCRIPT["<script type='application/json' id='webui-data'><br/>{ state: { count: 42
|
|
648
|
-
APPLY --> SEED["Write decorated fields
|
|
671
|
+
SCRIPT["<script type='application/json' id='webui-data'><br/>{ state: { count: 42 } }"] --> APPLY["$applySSRState()"]
|
|
672
|
+
APPLY --> SEED["Write decorated backing fields"]
|
|
649
673
|
SEED --> HYDRATE["$hydrate() — bindings match<br/>server-rendered DOM"]
|
|
650
674
|
```
|
|
651
675
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
676
|
+
Decorated writes go to the backing field (`_prop`) directly, avoiding reactive
|
|
677
|
+
updates before bindings are wired. For `@attr`, an existing SSR host attribute
|
|
678
|
+
takes precedence and the projected value is skipped. Template-only values
|
|
679
|
+
remain represented by the SSR DOM until browser state explicitly changes them.
|
|
680
|
+
Later `setState()` calls, including router partials, accept both decorated
|
|
681
|
+
properties and compiled template roots; undecorated roots are stored in hidden
|
|
682
|
+
framework state. The first write to a dormant HTML-only host replays only the
|
|
683
|
+
roots present in that write, preserving omitted SSR text, attributes,
|
|
684
|
+
conditions, and repeats.
|
|
656
685
|
|
|
657
686
|
---
|
|
658
687
|
|
|
659
688
|
## Repeat Reconciliation
|
|
660
689
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
C2["<todo-item> key=C ← reused"]
|
|
680
|
-
A2["<todo-item> key=A ← reused"]
|
|
681
|
-
B2["key=B ← removed"]
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
A1 -.->|"moved"| A2
|
|
685
|
-
C1 -.->|"moved"| C2
|
|
686
|
-
B1 -.->|"destroyed"| B2
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
### Sequential Reconciliation
|
|
690
|
-
|
|
691
|
-
When no keying attributes exist, items are matched by position. Excess items
|
|
692
|
-
are removed; new items are appended.
|
|
690
|
+
`<for>` blocks reconcile by array position by default. The existing block at
|
|
691
|
+
index `i` receives the current item at index `i`; only a new or removed tail
|
|
692
|
+
creates or removes blocks.
|
|
693
|
+
|
|
694
|
+
Duplicate values and attributes are safe because dynamic attributes never act
|
|
695
|
+
as hidden keys. Reordering rebinds existing blocks in place, so local
|
|
696
|
+
browser-owned or component state remains associated with positions rather than
|
|
697
|
+
logical items.
|
|
698
|
+
|
|
699
|
+
For reorderable or stateful lists, author `key="{{item.id}}"` on the first
|
|
700
|
+
child inside `<for>` to move existing blocks with their logical items.
|
|
701
|
+
`key="{{item}}"` supports arrays of unique string or finite-number primitives.
|
|
702
|
+
`key` is compiler-only metadata and is removed from SSR and client HTML;
|
|
703
|
+
`data-key` remains an ordinary attribute with no identity semantics. Key paths
|
|
704
|
+
are compiler-validated and stored only for explicitly keyed repeats, so
|
|
705
|
+
unkeyed bindings carry no key state or map allocation. Duplicate or invalid
|
|
706
|
+
runtime keys warn once, clear identity, and use positional reconciliation until
|
|
707
|
+
valid identity is re-established.
|
|
693
708
|
|
|
694
709
|
### SSR State Reading
|
|
695
710
|
|
|
@@ -763,7 +778,7 @@ This template-parallel traversal eliminates the need for any marker comments,
|
|
|
763
778
|
| Initial hydration | O(bindings) | Single pass over compiled path mappings |
|
|
764
779
|
| Reactive update | O(affected) | Per-path index skips unrelated bindings |
|
|
765
780
|
| Conditional toggle | O(block size) | Create/destroy a block instance |
|
|
766
|
-
| Repeat reconciliation | O(items) |
|
|
781
|
+
| Repeat reconciliation | O(items) | Positional scan; explicit keys use a reusable map only when order changes |
|
|
767
782
|
| Event wiring | O(events) | One-time during hydration |
|
|
768
783
|
|
|
769
784
|
### What the framework does NOT do
|
package/dist/decorators.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function toKebabCase(str: string): string;
|
|
2
2
|
export declare function getObservableNames(ctor: Function): Set<string>;
|
|
3
3
|
export declare function isAttributeProperty(ctor: Function, property: string): boolean;
|
|
4
|
+
export declare function attributeNameForProperty(ctor: Function, property: string): string | undefined;
|
|
4
5
|
export declare function syncAttrProperties(instance: object, ctor: Function): void;
|
|
5
6
|
export declare function observable(target: object, name: string): void;
|
|
6
7
|
export interface AttrOptions {
|
package/dist/decorators.js
CHANGED
|
@@ -94,6 +94,9 @@ function attrPropertyMapFor(ctor) {
|
|
|
94
94
|
export function isAttributeProperty(ctor, property) {
|
|
95
95
|
return attrPropertyMapFor(ctor)?.has(property) === true;
|
|
96
96
|
}
|
|
97
|
+
export function attributeNameForProperty(ctor, property) {
|
|
98
|
+
return attrPropertyMapFor(ctor)?.get(property)?.attribute;
|
|
99
|
+
}
|
|
97
100
|
function setReflectingAttribute(instance, attrName) {
|
|
98
101
|
instance[reflectingAttribute] = attrName;
|
|
99
102
|
}
|
package/dist/element/diff.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import type { RepeatBinding, RepeatHost } from './types.js';
|
|
1
|
+
import type { RepeatBinding, RepeatHost, RepeatKeyState } from './types.js';
|
|
2
2
|
export declare function dotWalk(cursor: unknown, path: string, from: number): unknown;
|
|
3
|
+
export declare function createRepeatKeyState(path: string): RepeatKeyState;
|
|
4
|
+
export declare function seedHydratedRepeatKeys(rep: RepeatBinding, items: unknown[]): void;
|
|
3
5
|
export declare function syncRepeat(host: RepeatHost, rep: RepeatBinding): void;
|
package/dist/element/diff.js
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
function asParent(node) {
|
|
2
|
-
if (!node)
|
|
3
|
-
return null;
|
|
4
|
-
return 'childNodes' in node ? node : null;
|
|
5
|
-
}
|
|
6
1
|
export function dotWalk(cursor, path, from) {
|
|
7
2
|
let start = from;
|
|
8
3
|
for (let i = from; i <= path.length; i++) {
|
|
@@ -15,111 +10,223 @@ export function dotWalk(cursor, path, from) {
|
|
|
15
10
|
}
|
|
16
11
|
return cursor;
|
|
17
12
|
}
|
|
18
|
-
function itemKey(item, keyPath) {
|
|
19
|
-
if (keyPath === undefined || keyPath === '')
|
|
20
|
-
return null;
|
|
21
|
-
const v = dotWalk(item, keyPath, 0);
|
|
22
|
-
return v != null ? String(v) : '';
|
|
23
|
-
}
|
|
24
13
|
function itemScope(rep, item) {
|
|
25
|
-
return { name: rep.itemVar, value: item, parent: rep.scope };
|
|
14
|
+
return { name: rep.itemVar, value: item, parent: rep.scope, known: true };
|
|
26
15
|
}
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
16
|
+
export function createRepeatKeyState(path) {
|
|
17
|
+
return {
|
|
18
|
+
path,
|
|
19
|
+
warned: false,
|
|
20
|
+
keys: [],
|
|
21
|
+
nextKeys: [],
|
|
22
|
+
map: new Map(),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function seedHydratedRepeatKeys(rep, items) {
|
|
26
|
+
const state = rep.keyState;
|
|
27
|
+
if (!state || items.length !== rep.instances.length)
|
|
34
28
|
return;
|
|
35
|
-
|
|
36
|
-
if (!rep.synced && items.length === 0 && rep.instances.length > 0)
|
|
29
|
+
if (!collectRepeatKeys(items, state))
|
|
37
30
|
return;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
rep.instances = [];
|
|
31
|
+
commitKeyIdentity(state);
|
|
32
|
+
}
|
|
33
|
+
function setItemScope(instance, item) {
|
|
34
|
+
if (!instance.scope)
|
|
44
35
|
return;
|
|
36
|
+
instance.scope.value = item;
|
|
37
|
+
instance.scope.known = true;
|
|
38
|
+
}
|
|
39
|
+
function syncPositional(host, rep, items, container) {
|
|
40
|
+
const instances = rep.instances;
|
|
41
|
+
const oldLength = instances.length;
|
|
42
|
+
const reuseCount = Math.min(oldLength, items.length);
|
|
43
|
+
let nextCount = reuseCount;
|
|
44
|
+
let created = false;
|
|
45
|
+
for (let i = 0; i < reuseCount; i += 1) {
|
|
46
|
+
setItemScope(instances[i], items[i]);
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
let nextCount = reuseCount;
|
|
53
|
-
for (let i = 0; i < reuseCount; i += 1) {
|
|
54
|
-
const entry = oldInstances[i];
|
|
55
|
-
entry.value = items[i];
|
|
56
|
-
if (entry.instance.scope)
|
|
57
|
-
entry.instance.scope.value = items[i];
|
|
58
|
-
}
|
|
59
|
-
for (let i = reuseCount; i < items.length; i += 1) {
|
|
60
|
-
const scope = itemScope(rep, items[i]);
|
|
61
|
-
const instance = host.$createBlockInstance(rep.blockIndex, scope);
|
|
62
|
-
if (instance) {
|
|
63
|
-
oldInstances[nextCount] = { key: null, value: items[i], instance };
|
|
64
|
-
nextCount += 1;
|
|
65
|
-
}
|
|
48
|
+
for (let i = reuseCount; i < items.length; i += 1) {
|
|
49
|
+
const instance = host.$createBlockInstance(rep.blockIndex, itemScope(rep, items[i]), rep.owner, container);
|
|
50
|
+
if (instance) {
|
|
51
|
+
instances[nextCount] = instance;
|
|
52
|
+
nextCount += 1;
|
|
53
|
+
created = true;
|
|
66
54
|
}
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
}
|
|
56
|
+
for (let i = reuseCount; i < oldLength; i += 1) {
|
|
57
|
+
host.$removeInstance(instances[i]);
|
|
58
|
+
}
|
|
59
|
+
instances.length = nextCount;
|
|
60
|
+
const removed = oldLength > reuseCount;
|
|
61
|
+
let cursor = rep.start;
|
|
62
|
+
for (let i = 0; i < instances.length; i += 1) {
|
|
63
|
+
cursor = host.$insertInstanceAfter(cursor, container, instances[i]);
|
|
64
|
+
}
|
|
65
|
+
for (let i = 0; i < reuseCount; i += 1) {
|
|
66
|
+
host.$updateInstance(instances[i]);
|
|
67
|
+
}
|
|
68
|
+
if (created || removed)
|
|
69
|
+
host.$changeStructure(removed ? rep.owner : undefined);
|
|
70
|
+
}
|
|
71
|
+
function readRepeatKey(item, path) {
|
|
72
|
+
if (path.length === 0)
|
|
73
|
+
return item;
|
|
74
|
+
return dotWalk(item, path, 0);
|
|
75
|
+
}
|
|
76
|
+
function isRepeatKey(value) {
|
|
77
|
+
return (typeof value === 'string' ||
|
|
78
|
+
(typeof value === 'number' && Number.isFinite(value)));
|
|
79
|
+
}
|
|
80
|
+
function collectRepeatKeys(items, state) {
|
|
81
|
+
const keys = state.nextKeys;
|
|
82
|
+
const seen = state.map;
|
|
83
|
+
keys.length = items.length;
|
|
84
|
+
seen.clear();
|
|
85
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
86
|
+
let value;
|
|
87
|
+
try {
|
|
88
|
+
value = readRepeatKey(items[i], state.path);
|
|
69
89
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
catch {
|
|
91
|
+
keys.length = 0;
|
|
92
|
+
seen.clear();
|
|
93
|
+
return false;
|
|
74
94
|
}
|
|
75
|
-
|
|
76
|
-
|
|
95
|
+
if (!isRepeatKey(value) || seen.has(value)) {
|
|
96
|
+
keys.length = 0;
|
|
97
|
+
seen.clear();
|
|
98
|
+
return false;
|
|
77
99
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const oldByKey = new Map();
|
|
81
|
-
for (let i = 0; i < oldInstances.length; i += 1) {
|
|
82
|
-
const entry = oldInstances[i];
|
|
83
|
-
const k = entry.key;
|
|
84
|
-
if (k != null)
|
|
85
|
-
oldByKey.set(k, entry);
|
|
100
|
+
keys[i] = value;
|
|
101
|
+
seen.set(value, null);
|
|
86
102
|
}
|
|
103
|
+
seen.clear();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
function commitKeyIdentity(state) {
|
|
107
|
+
const oldKeys = state.keys;
|
|
108
|
+
state.keys = state.nextKeys;
|
|
109
|
+
state.nextKeys = oldKeys;
|
|
110
|
+
state.nextKeys.length = 0;
|
|
111
|
+
}
|
|
112
|
+
function clearKeyIdentity(state) {
|
|
113
|
+
state.keys.length = 0;
|
|
114
|
+
state.nextKeys.length = 0;
|
|
115
|
+
state.map.clear();
|
|
116
|
+
}
|
|
117
|
+
function warnKeyFallback(rep, state) {
|
|
118
|
+
if (state.warned)
|
|
119
|
+
return;
|
|
120
|
+
state.warned = true;
|
|
121
|
+
const key = state.path.length === 0
|
|
122
|
+
? rep.itemVar
|
|
123
|
+
: `${rep.itemVar}.${state.path}`;
|
|
124
|
+
console.warn(`[webui] repeat "${rep.collection}" produced duplicate or invalid values for child key="${key}"; using positional reconciliation`);
|
|
125
|
+
}
|
|
126
|
+
function reconcileByKey(host, rep, items, container, state) {
|
|
127
|
+
const instances = rep.instances;
|
|
128
|
+
const map = state.map;
|
|
87
129
|
let nextCount = 0;
|
|
130
|
+
let created = false;
|
|
131
|
+
let removed = false;
|
|
88
132
|
for (let i = 0; i < items.length; i += 1) {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
existing.value = item;
|
|
95
|
-
existing.key = key;
|
|
96
|
-
if (existing.instance.scope)
|
|
97
|
-
existing.instance.scope.value = item;
|
|
98
|
-
oldInstances[nextCount] = existing;
|
|
99
|
-
nextCount += 1;
|
|
133
|
+
const key = state.nextKeys[i];
|
|
134
|
+
let instance = map.get(key);
|
|
135
|
+
if (instance) {
|
|
136
|
+
map.set(key, null);
|
|
137
|
+
setItemScope(instance, items[i]);
|
|
100
138
|
}
|
|
101
139
|
else {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
140
|
+
instance = host.$createBlockInstance(rep.blockIndex, itemScope(rep, items[i]), rep.owner, container) ?? undefined;
|
|
141
|
+
if (instance)
|
|
142
|
+
created = true;
|
|
143
|
+
}
|
|
144
|
+
if (instance) {
|
|
145
|
+
instances[nextCount] = instance;
|
|
146
|
+
state.nextKeys[nextCount] = key;
|
|
147
|
+
nextCount += 1;
|
|
108
148
|
}
|
|
109
149
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
150
|
+
state.nextKeys.length = nextCount;
|
|
151
|
+
for (const instance of map.values()) {
|
|
152
|
+
if (instance) {
|
|
153
|
+
host.$removeInstance(instance);
|
|
154
|
+
removed = true;
|
|
155
|
+
}
|
|
113
156
|
}
|
|
114
|
-
oldInstances.length = nextCount;
|
|
115
157
|
let cursor = rep.start;
|
|
116
|
-
for (let i = 0; i <
|
|
117
|
-
cursor = host.$insertInstanceAfter(cursor, container,
|
|
158
|
+
for (let i = 0; i < nextCount; i += 1) {
|
|
159
|
+
cursor = host.$insertInstanceAfter(cursor, container, instances[i]);
|
|
160
|
+
}
|
|
161
|
+
for (let i = 0; i < nextCount; i += 1) {
|
|
162
|
+
if (map.get(state.nextKeys[i]) === null) {
|
|
163
|
+
host.$updateInstance(instances[i]);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
map.clear();
|
|
167
|
+
instances.length = nextCount;
|
|
168
|
+
commitKeyIdentity(state);
|
|
169
|
+
if (created || removed)
|
|
170
|
+
host.$changeStructure(removed ? rep.owner : undefined);
|
|
171
|
+
}
|
|
172
|
+
function syncKeyed(host, rep, items, container, state) {
|
|
173
|
+
if (!collectRepeatKeys(items, state)) {
|
|
174
|
+
warnKeyFallback(rep, state);
|
|
175
|
+
clearKeyIdentity(state);
|
|
176
|
+
syncPositional(host, rep, items, container);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
let sharedOrder = true;
|
|
180
|
+
const sharedLength = Math.min(state.keys.length, state.nextKeys.length);
|
|
181
|
+
for (let i = 0; sharedOrder && i < sharedLength; i += 1) {
|
|
182
|
+
sharedOrder = state.keys[i] === state.nextKeys[i];
|
|
118
183
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
184
|
+
if (state.keys.length !== rep.instances.length || sharedOrder) {
|
|
185
|
+
syncPositional(host, rep, items, container);
|
|
186
|
+
if (rep.instances.length === items.length) {
|
|
187
|
+
commitKeyIdentity(state);
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
clearKeyIdentity(state);
|
|
123
191
|
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
for (let i = 0; i < state.keys.length; i += 1) {
|
|
195
|
+
state.map.set(state.keys[i], rep.instances[i]);
|
|
196
|
+
}
|
|
197
|
+
reconcileByKey(host, rep, items, container, state);
|
|
198
|
+
}
|
|
199
|
+
export function syncRepeat(host, rep) {
|
|
200
|
+
const resolved = host.$resolveValue(rep.collection, rep.scope);
|
|
201
|
+
const items = Array.isArray(resolved) ? resolved : [];
|
|
202
|
+
const container = (rep.container
|
|
203
|
+
?? rep.start?.parentNode
|
|
204
|
+
?? rep.owner.nodes[0]?.parentNode);
|
|
205
|
+
if (!container)
|
|
206
|
+
return;
|
|
207
|
+
rep.container = container;
|
|
208
|
+
if (!rep.synced
|
|
209
|
+
&& rep.instances.length > 0
|
|
210
|
+
&& !host.$hasStateRoot(rep.collection, rep.scope))
|
|
211
|
+
return;
|
|
212
|
+
rep.synced = true;
|
|
213
|
+
if (items.length === 0) {
|
|
214
|
+
const hadInstances = rep.instances.length !== 0;
|
|
215
|
+
for (let i = 0; i < rep.instances.length; i += 1) {
|
|
216
|
+
host.$removeInstance(rep.instances[i]);
|
|
217
|
+
}
|
|
218
|
+
rep.instances.length = 0;
|
|
219
|
+
if (hadInstances)
|
|
220
|
+
host.$changeStructure(rep.owner);
|
|
221
|
+
if (rep.keyState) {
|
|
222
|
+
clearKeyIdentity(rep.keyState);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (rep.keyState) {
|
|
227
|
+
syncKeyed(host, rep, items, container, rep.keyState);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
syncPositional(host, rep, items, container);
|
|
124
231
|
}
|
|
125
232
|
}
|
package/dist/element/markers.js
CHANGED
|
@@ -6,16 +6,24 @@ const MARKER_REPEAT_ITEM = 'wi';
|
|
|
6
6
|
export function collectItemMarkers(repeatStart) {
|
|
7
7
|
const items = [];
|
|
8
8
|
let end = null;
|
|
9
|
+
let depth = 1;
|
|
9
10
|
let node = repeatStart.nextSibling;
|
|
10
11
|
while (node) {
|
|
11
12
|
if (node.nodeType === 8) {
|
|
12
13
|
const data = node.data;
|
|
13
|
-
if (data ===
|
|
14
|
-
|
|
15
|
-
break;
|
|
14
|
+
if (data === MARKER_REPEAT_START) {
|
|
15
|
+
depth++;
|
|
16
16
|
}
|
|
17
|
-
if (data ===
|
|
17
|
+
else if (data === MARKER_REPEAT_END) {
|
|
18
|
+
depth--;
|
|
19
|
+
if (depth === 0) {
|
|
20
|
+
end = node;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (data === MARKER_REPEAT_ITEM && depth === 1) {
|
|
18
25
|
items.push(node);
|
|
26
|
+
}
|
|
19
27
|
}
|
|
20
28
|
node = node.nextSibling;
|
|
21
29
|
}
|
|
@@ -28,7 +36,7 @@ export function nextElement(marker) {
|
|
|
28
36
|
return node;
|
|
29
37
|
if (node.nodeType === 8) {
|
|
30
38
|
const data = node.data;
|
|
31
|
-
if (data === MARKER_REPEAT_END || data === MARKER_REPEAT_ITEM)
|
|
39
|
+
if (data === MARKER_REPEAT_END || data === MARKER_REPEAT_ITEM || data === MARKER_COND_END)
|
|
32
40
|
return null;
|
|
33
41
|
}
|
|
34
42
|
node = node.nextSibling;
|