@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 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
- When HTML-only components receive server or route state, import
96
- `@microsoft/webui-framework` somewhere in the browser entry. The framework root
97
- installs the static host runtime, which only claims compiler-owned HTML-only
98
- components whose templates need hidden state or observed host attributes. Fully
99
- static HTML-only components stay as plain SSR DOM.
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, or once from the browser entry when the app has no authored
115
- components but still uses HTML-only components that receive server or route
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; HTML-only components are declarative only.
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 for HTML-only components that only need template
306
- bindings and router/server state.
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 (~130 lines)<br/><i>List Reconciliation</i><br/>keyed/sequential diffing<br/>for @for repeat blocks"]
462
+ DIFF["element/diff.ts<br/><i>List Reconciliation</i><br/>positional + explicit-key diffing<br/>for &lt;for&gt; 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 + template state
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
- th?: 1; // Compiler-owned static host
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` — a JSON object loaded from the
639
- server-emitted `#webui-data` block. Like Preact's props, this delivers the
640
- same data used for SSR rendering to the client. During `$mount()`,
641
- `$applySSRState()` writes matching decorated keys directly to observable backing
642
- fields and stores undecorated template roots in hidden framework state before
643
- any bindings are wired:
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["&lt;script type='application/json' id='webui-data'&gt;<br/>{ state: { count: 42, title: 'Hello' } }"] --> APPLY["$applySSRState()"]
648
- APPLY --> SEED["Write decorated fields + hidden template state"]
671
+ SCRIPT["&lt;script type='application/json' id='webui-data'&gt;<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
- `$applySSRState()` only accepts keys that are decorated properties or compiled
653
- template roots. Unknown keys are ignored. Decorated writes go to the backing
654
- field (`_prop`) directly, and undecorated template roots stay internal, avoiding
655
- reactive updates before bindings are wired.
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
- `@for(item of items)` blocks support two reconciliation strategies,
662
- implemented in `element/diff.ts` (~130 lines):
663
-
664
- ### Keyed Reconciliation
665
-
666
- When the repeat block's root element has attribute bindings (e.g.
667
- `<todo-item id="{{item.id}}">`), the framework uses the first attribute as a
668
- key. This preserves DOM nodes across reorders:
669
-
670
- ```mermaid
671
- flowchart TD
672
- subgraph Before ["Before: items = [A, B, C]"]
673
- A1["&lt;todo-item&gt; key=A"]
674
- B1["&lt;todo-item&gt; key=B"]
675
- C1["&lt;todo-item&gt; key=C"]
676
- end
677
-
678
- subgraph After ["After: items = [C, A]"]
679
- C2["&lt;todo-item&gt; key=C ← reused"]
680
- A2["&lt;todo-item&gt; 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) | Keyed map lookup or sequential scan |
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
@@ -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 {
@@ -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
  }
@@ -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;
@@ -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 syncRepeat(host, rep) {
28
- const resolved = host.$resolveValue(rep.collection, rep.scope);
29
- const items = Array.isArray(resolved) ? resolved : [];
30
- let container = rep.container
31
- ?? (rep.start ? asParent(rep.start.parentNode) : null)
32
- ?? (rep.owner.nodes[0] ? asParent(rep.owner.nodes[0].parentNode) : null);
33
- if (!container)
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
- rep.container = container;
36
- if (!rep.synced && items.length === 0 && rep.instances.length > 0)
29
+ if (!collectRepeatKeys(items, state))
37
30
  return;
38
- rep.synced = true;
39
- if (items.length === 0) {
40
- for (let i = 0; i < rep.instances.length; i += 1) {
41
- host.$removeInstance(rep.instances[i].instance);
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
- const keyPath = rep.keyPath;
47
- const hasKeys = keyPath !== undefined && keyPath !== '';
48
- const oldInstances = rep.instances;
49
- if (!hasKeys) {
50
- const oldLength = oldInstances.length;
51
- const reuseCount = Math.min(oldLength, items.length);
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
- for (let i = reuseCount; i < oldLength; i += 1) {
68
- host.$removeInstance(oldInstances[i].instance);
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
- oldInstances.length = nextCount;
71
- let cursor = rep.start;
72
- for (let i = 0; i < oldInstances.length; i += 1) {
73
- cursor = host.$insertInstanceAfter(cursor, container, oldInstances[i].instance);
90
+ catch {
91
+ keys.length = 0;
92
+ seen.clear();
93
+ return false;
74
94
  }
75
- for (let i = 0; i < reuseCount; i += 1) {
76
- host.$updateInstance(oldInstances[i].instance);
95
+ if (!isRepeatKey(value) || seen.has(value)) {
96
+ keys.length = 0;
97
+ seen.clear();
98
+ return false;
77
99
  }
78
- return;
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 item = items[i];
90
- const key = itemKey(item, keyPath);
91
- const existing = key != null ? oldByKey.get(key) : undefined;
92
- if (existing) {
93
- oldByKey.set(key, undefined);
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
- const scope = itemScope(rep, item);
103
- const instance = host.$createBlockInstance(rep.blockIndex, scope);
104
- if (instance) {
105
- oldInstances[nextCount] = { key: key ?? null, value: item, instance };
106
- nextCount += 1;
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
- for (const leftover of oldByKey.values()) {
111
- if (leftover)
112
- host.$removeInstance(leftover.instance);
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 < oldInstances.length; i += 1) {
117
- cursor = host.$insertInstanceAfter(cursor, container, oldInstances[i].instance);
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
- for (let i = 0; i < oldInstances.length; i += 1) {
120
- const entry = oldInstances[i];
121
- if (entry.key != null && oldByKey.has(entry.key) && oldByKey.get(entry.key) === undefined) {
122
- host.$updateInstance(entry.instance);
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
  }
@@ -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 === MARKER_REPEAT_END) {
14
- end = node;
15
- break;
14
+ if (data === MARKER_REPEAT_START) {
15
+ depth++;
16
16
  }
17
- if (data === MARKER_REPEAT_ITEM)
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;