micra.js 2.2.0 → 2.3.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,94 @@ All notable changes to Micra.js will be documented in this file. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows
5
5
  [SemVer](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [2.3.0] — 2026-05-30
8
+
9
+ ### TypeScript — type-safe event bus
10
+
11
+ - **New augmentable `MicraEvents` interface.** Declare your app's events
12
+ once and `Micra.emit` / `Micra.on` / `this.emit` / `this.on` enforce
13
+ payload types and arity at the call site:
14
+
15
+ ```ts
16
+ declare module 'micra.js' {
17
+ interface MicraEvents {
18
+ 'cart:updated': { count: number }
19
+ 'modal:close': void
20
+ }
21
+ }
22
+
23
+ Micra.emit('cart:updated', { count: 3 }) // ✓
24
+ Micra.emit('cart:updated', { count: '3' }) // ✗ type error
25
+ Micra.emit('modal:close') // ✓ void → no args
26
+ ```
27
+
28
+ - Events that are NOT declared in `MicraEvents` keep the previous
29
+ behaviour — payload typed as `unknown`, optional argument. Untyped
30
+ code keeps compiling unchanged.
31
+ - New exported types: `MicraEvents`, `EventPayload<K>`, `EmitArgs<K>`.
32
+ - Bundle stays at **5.4 KB gzip** — types only, no runtime change.
33
+
34
+ ### Breaking — types only
35
+
36
+ - The legacy `on<T>(event, handler)` generic now infers `T` as the
37
+ event *key*, not the handler payload. Code that explicitly passed a
38
+ payload type via the generic (`Micra.on<User>('user:updated', h)`)
39
+ still compiles, but `h`'s parameter falls back to `unknown` unless
40
+ the event is declared in `MicraEvents`. Migration: register the
41
+ event via `declare module 'micra.js'` and drop the explicit generic.
42
+ No runtime impact.
43
+
44
+ ### Performance — non-keyed `data-each` now reuses DOM nodes
45
+
46
+ - **Non-keyed `<template data-each>` no longer re-renders the whole list
47
+ on every update.** The new path keeps the first `min(prev, next)` row
48
+ nodes in place — only the length delta is touched (tail removed when
49
+ the list shrinks, new rows cloned when it grows). Each retained row
50
+ gets a fresh `itemState` and a re-applied directive pass through its
51
+ cached `__micraScan`, so content updates correctly without the
52
+ remove/re-clone overhead.
53
+ - Row identity is now stable across renders for the no-key path: event
54
+ listeners bound via `data-on` / `@event` / `data-model` survive
55
+ re-renders without re-binding, and DOM-level state (focus, scroll,
56
+ CSS transitions) is preserved on retained rows.
57
+ - Items that didn't change (same reference + same index) skip
58
+ `applyDirectives` entirely when only the `data-each` source array is
59
+ the trigger for this render cycle — same `canSkipUnchanged`
60
+ optimisation the keyed path already had.
61
+ - Bundle: **5.5 KB gzip** (raised guard from 5.4 → 5.5 to give the
62
+ shared row-creation helper room; net code is slightly smaller after
63
+ factoring `createRowNode` out of both keyed and non-keyed paths).
64
+
65
+ ### Breaking — non-keyed multi-root rows now wrap in `<micra-each-item>`
66
+
67
+ - Templates whose `data-each` content has more than one top-level node
68
+ now render each row inside a `<micra-each-item style="display:contents">`
69
+ wrapper, mirroring the keyed path's existing behaviour. The wrapper is
70
+ visually inert (CSS `display:contents` opts out of the box model) but
71
+ it does add one node to the parse tree.
72
+ - Impact:
73
+ - **CSS:** child selectors that targeted `parent > .row` will now
74
+ match `parent > micra-each-item` instead. Use descendant selectors
75
+ (`parent .row`) or update the rules.
76
+ - **Invalid HTML contexts:** templates whose rows are `<tr>` / `<td>` /
77
+ `<li>` inside `<tbody>` / `<tr>` / `<ul>` cannot legally have a
78
+ wrapper between the parent and the row. Hoist the wrapper into the
79
+ template (so the row is single-rooted) or use a `data-key`.
80
+ - Single-root templates are unchanged — by far the common case.
81
+
82
+ ## [2.2.1] — 2026-05-28
83
+
84
+ ### Performance — batched first list render
85
+
86
+ - **First render of a keyed `data-each` list now inserts in a single DOM
87
+ operation.** `renderKeyed` previously appended each new row with an
88
+ individual `anchor.after(node)` call — N insertions for an N-row list. On the
89
+ initial render (no previous rows to diff against), all freshly-cloned rows are
90
+ now collected into one `DocumentFragment` and inserted with a single
91
+ `marker.after()`, skipping the LIS reorder pass entirely. The update, swap, and
92
+ reorder paths are unchanged.
93
+ - No public-API change. Bundle stays at **5.4 KB gzip**.
94
+
7
95
  ## [2.2.0] — 2026-05-27
8
96
 
9
97
  ### Performance — single-pass DOM scan
package/README.md CHANGED
@@ -182,6 +182,7 @@ this.on(event, handler)
182
182
  - Recipes:
183
183
  - [Todo app](./docs/recipes/todo-app.md)
184
184
  - [Server-sent events (SSE)](./docs/recipes/sse.md)
185
+ - [htmx bridge](./docs/recipes/htmx.md)
185
186
 
186
187
  ## Code generation with LLMs
187
188
 
@@ -10,23 +10,25 @@
10
10
  * Component instances subscribe via `instance.on()` which auto-registers
11
11
  * the unsub token in `instance.__micraSubs` for cleanup on destroy().
12
12
  */
13
- import type { EventHandler, UnsubFn } from '../types';
13
+ import type { EmitArgs, EventPayload, UnsubFn } from '../types';
14
14
  /**
15
15
  * Subscribe to a named event. Returns an unsubscribe function.
16
+ * Payload is typed via the `MicraEvents` interface (augmentable).
16
17
  *
17
18
  * @example
18
19
  * const unsub = on('user:login', (user) => console.log(user))
19
20
  * unsub() // stop listening
20
21
  */
21
- export declare function on<T = unknown>(event: string, handler: EventHandler<T>): UnsubFn;
22
+ export declare function on<K extends string>(event: K, handler: (payload: EventPayload<K>) => void): UnsubFn;
22
23
  /**
23
24
  * Unsubscribe a specific handler from an event.
24
25
  */
25
- export declare function off(event: string, handler: EventHandler): void;
26
+ export declare function off<K extends string>(event: K, handler: (payload: EventPayload<K>) => void): void;
26
27
  /**
27
28
  * Publish an event to all subscribers. Errors are caught per-handler.
29
+ * Payload is typed via the `MicraEvents` interface (augmentable).
28
30
  *
29
31
  * @example
30
32
  * emit('user:updated', { id: 1, name: 'Alice' })
31
33
  */
32
- export declare function emit(event: string, payload?: unknown): void;
34
+ export declare function emit<K extends string>(event: K, ...args: EmitArgs<K>): void;
@@ -18,7 +18,7 @@ import type { StateRecord } from '../types';
18
18
  * const state = createReactiveState(raw, render)
19
19
  * state.count = 5 // triggers render() in next microtask
20
20
  */
21
- export declare function createReactiveState<S extends StateRecord>(obj: S, schedule: () => void): S;
21
+ export declare function createReactiveState<S extends StateRecord>(obj: S, schedule: () => void, onKey?: (key: string) => void): S;
22
22
  /**
23
23
  * Return a debounce function that defers `render` to the next microtask.
24
24
  * Multiple calls within the same tick collapse to a single render.
@@ -4,7 +4,8 @@
4
4
  * Responsibilities:
5
5
  * - Process `<template data-each="items" data-key="id">` elements
6
6
  * - Keyed diff: reuse/reorder DOM nodes by key — O(n) with a Map
7
- * - Non-keyed fallback: full replace (no key → warn in dev, full re-render)
7
+ * - Non-keyed fallback: length-based positional reuse min(old, new) rows
8
+ * are kept as-is, the tail is removed or new rows are appended
8
9
  * - Apply directives to each row with a scoped itemState
9
10
  *
10
11
  * LLM NOTE: renderList() is called on every render cycle AFTER applyDirectives().
@@ -12,16 +13,19 @@
12
13
  * Each row node gets its own ScanIndex cached on `node.__micraScan` so
13
14
  * re-renders of that row don't re-walk the DOM.
14
15
  * Keyed mode (data-key present) mutates the DOM in-place — nodes are
15
- * created once and reused. Non-keyed mode removes all nodes and re-clones.
16
+ * created once and reused. Non-keyed mode also reuses existing nodes
17
+ * positionally: only the length delta is touched, the rest gets a fresh
18
+ * itemState and re-applies directives.
16
19
  */
17
20
  import type { InternalInstance, StateRecord } from '../types';
18
21
  /**
19
22
  * Process all `<template data-each>` elements found by the scanner.
20
23
  * Scoped itemState makes `item`, `index`, `$index` available in row expressions.
21
24
  *
22
- * @param templates - Pre-scanned list of <template data-each> elements
23
- * @param state - Expression state (proxy merging rawState + instance)
24
- * @param rawState - Raw (non-proxy) state — used for model binding
25
- * @param instance - Component instance (for event binding)
25
+ * @param templates - Pre-scanned list of <template data-each> elements
26
+ * @param state - Expression state (proxy merging rawState + instance)
27
+ * @param rawState - Raw (non-proxy) state — used for model binding
28
+ * @param instance - Component instance (for event binding)
29
+ * @param triggerKey - Which state key triggered this render (null = initial, 'MULTIPLE' = batch)
26
30
  */
27
- export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>): void;
31
+ export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>, triggerKey: string | null | 'MULTIPLE'): void;
@@ -10,7 +10,8 @@
10
10
  * even *visit* those nodes.
11
11
  * - <template> contents are not visited (browser TreeWalker default).
12
12
  * `<template data-each>` itself IS visited and classified into scan.each;
13
- * its children are processed by each.ts on every render via scanFragment.
13
+ * its children are processed by each.ts on every render fresh rows
14
+ * are wrapped in a per-row element and scanned via scanComponent.
14
15
  *
15
16
  * Hot-path notes:
16
17
  * - We read `el.attributes` once and switch by suffix. No allocations per
@@ -27,8 +28,3 @@ import type { ScanIndex } from "../types";
27
28
  * are free.
28
29
  */
29
30
  export declare function scanComponent(root: Element): ScanIndex;
30
- /**
31
- * Scan a DocumentFragment (no-key each clone). Not cached — these fragments
32
- * are temporary and re-cloned every render.
33
- */
34
- export declare function scanFragment(frag: DocumentFragment): ScanIndex;
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  *
22
22
  * @module Micra
23
23
  */
24
- export type { StateRecord, UnsubFn, EventHandler, FetchOptions, ComponentMethods, ComponentBuiltins, ComponentInstance, ComponentDefinition, } from './types';
24
+ export type { StateRecord, UnsubFn, EventHandler, EventPayload, EmitArgs, MicraEvents, FetchOptions, ComponentMethods, ComponentBuiltins, ComponentInstance, ComponentDefinition, } from './types';
25
25
  export { FetchError } from './utils/fetch';
26
26
  export { define, defineComponent, instances, registry, debug } from './core/registry';
27
27
  export { mount } from './core/mount';
package/dist/micra.cjs.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v2.2.0 — https://github.com/micra-js/micra — MIT */
1
+ /* Micra.js v2.3.0 — https://github.com/micra-js/micra — MIT */
2
2
  "use strict";
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -176,27 +176,32 @@ function safeStateHas(state, key) {
176
176
  return false;
177
177
  }
178
178
  function evalExpr(expr, state) {
179
- if (SIMPLE_PATH.test(expr)) {
180
- const parts = expr.split(".");
181
- if (!safeStateHas(state, parts[0])) return void 0;
182
- return parts.reduce(
179
+ let cached = exprCache.get(expr);
180
+ if (!cached) {
181
+ if (SIMPLE_PATH.test(expr)) {
182
+ cached = { kind: "path", parts: expr.split(".") };
183
+ } else {
184
+ try {
185
+ cached = {
186
+ kind: "fn",
187
+ fn: new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
188
+ };
189
+ } catch {
190
+ warn(`invalid expression "${expr}"`);
191
+ cached = { kind: "fn", fn: () => void 0 };
192
+ }
193
+ }
194
+ exprCache.set(expr, cached);
195
+ }
196
+ if (cached.kind === "path") {
197
+ if (!safeStateHas(state, cached.parts[0])) return void 0;
198
+ return cached.parts.reduce(
183
199
  (obj, key) => obj != null ? obj[key] : void 0,
184
200
  state
185
201
  );
186
202
  }
187
- if (!exprCache.has(expr)) {
188
- try {
189
- exprCache.set(
190
- expr,
191
- new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
192
- );
193
- } catch {
194
- warn(`invalid expression "${expr}"`);
195
- exprCache.set(expr, () => void 0);
196
- }
197
- }
198
203
  try {
199
- return exprCache.get(expr)(safeStateWrap(state), SAFE_OUTER);
204
+ return cached.fn(safeStateWrap(state), SAFE_OUTER);
200
205
  } catch (e) {
201
206
  if (!warnedRuntime.has(expr)) {
202
207
  warnedRuntime.add(expr);
@@ -222,8 +227,9 @@ function off(event, handler) {
222
227
  set.delete(handler);
223
228
  if (set.size === 0) _bus.delete(event);
224
229
  }
225
- function emit(event, payload) {
230
+ function emit(event, ...args) {
226
231
  var _a;
232
+ const payload = args[0];
227
233
  (_a = _bus.get(event)) == null ? void 0 : _a.forEach((h) => {
228
234
  try {
229
235
  h(payload);
@@ -234,11 +240,12 @@ function emit(event, payload) {
234
240
  }
235
241
 
236
242
  // src/core/reactive.ts
237
- function createReactiveState(obj, schedule) {
243
+ function createReactiveState(obj, schedule, onKey) {
238
244
  return new Proxy(obj, {
239
245
  set(target, key, value) {
240
246
  ;
241
247
  target[key] = value;
248
+ onKey == null ? void 0 : onKey(key);
242
249
  schedule();
243
250
  return true;
244
251
  }
@@ -264,7 +271,8 @@ function applyText(el, expr, state) {
264
271
  }
265
272
  function applyHtml(el, expr, state) {
266
273
  var _a;
267
- el.innerHTML = String((_a = evalExpr(expr, state)) != null ? _a : "");
274
+ const html = String((_a = evalExpr(expr, state)) != null ? _a : "");
275
+ if (el.innerHTML !== html) el.innerHTML = html;
268
276
  }
269
277
  function applyIf(binding, state) {
270
278
  const el = binding.el;
@@ -281,7 +289,9 @@ function applyIf(binding, state) {
281
289
  }
282
290
  }
283
291
  function applyShow(el, expr, state) {
284
- el.style.display = evalExpr(expr, state) ? "" : "none";
292
+ const desired = evalExpr(expr, state) ? "" : "none";
293
+ const htmlEl = el;
294
+ if (htmlEl.style.display !== desired) htmlEl.style.display = desired;
285
295
  }
286
296
  function applyBind(el, pairs, state) {
287
297
  for (const [attr, valExpr] of pairs) {
@@ -526,23 +536,9 @@ function scanComponent(root) {
526
536
  }
527
537
  return scan;
528
538
  }
529
- function scanFragment(frag) {
530
- const scan = emptyScan();
531
- const walker = document.createTreeWalker(
532
- frag,
533
- NodeFilter.SHOW_ELEMENT,
534
- NESTED_COMPONENT_FILTER
535
- );
536
- let node = walker.nextNode();
537
- while (node) {
538
- classify(node, scan);
539
- node = walker.nextNode();
540
- }
541
- return scan;
542
- }
543
539
 
544
540
  // src/dom/each.ts
545
- function renderList(templates, state, rawState, instance) {
541
+ function renderList(templates, state, rawState, instance, triggerKey) {
546
542
  var _a;
547
543
  for (const tmplEl of templates) {
548
544
  if (tmplEl.tagName !== "TEMPLATE") continue;
@@ -559,22 +555,40 @@ function renderList(templates, state, rawState, instance) {
559
555
  }
560
556
  const marker = tmpl.__micraMarker;
561
557
  const keyMap = tmpl.__micraNodes;
562
- const parent = marker.parentNode;
563
- if (!parent) continue;
558
+ if (!marker.parentNode) continue;
564
559
  if (!Array.isArray(items)) {
565
560
  tmpl.__micraList.forEach((n) => n.remove());
566
561
  tmpl.__micraList = [];
567
562
  keyMap.clear();
568
563
  continue;
569
564
  }
565
+ const canSkipUnchanged = triggerKey !== null && triggerKey !== "MULTIPLE" && triggerKey === itemsExpr;
570
566
  if (keyAttr) {
571
- renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance);
567
+ renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
572
568
  } else {
573
- renderNoKey(tmpl, items, marker, parent, state, rawState, instance);
569
+ renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged);
574
570
  }
575
571
  }
576
572
  }
577
- function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
573
+ function createRowNode(tmpl, state, instance) {
574
+ const frag = tmpl.content.cloneNode(true);
575
+ let node;
576
+ if (frag.childNodes.length === 1) {
577
+ node = frag.firstElementChild;
578
+ } else {
579
+ node = document.createElement("micra-each-item");
580
+ node.style.display = "contents";
581
+ node.append(frag);
582
+ }
583
+ const rowScan = scanComponent(node);
584
+ node.__micraScan = rowScan;
585
+ node._itemState = Object.create(state);
586
+ bindDataOn(rowScan.on, instance);
587
+ bindAtEvents(rowScan.atEvents, instance);
588
+ bindModels(rowScan.model, instance);
589
+ return node;
590
+ }
591
+ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged) {
578
592
  var _a;
579
593
  const nextKeys = /* @__PURE__ */ new Set();
580
594
  const nextNodes = [];
@@ -593,26 +607,19 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
593
607
  nextKeys.add(key);
594
608
  let node = keyMap.get(key);
595
609
  if (!node) {
596
- const frag = tmpl.content.cloneNode(true);
597
- if (frag.childNodes.length === 1) {
598
- node = frag.firstElementChild;
599
- } else {
600
- node = document.createElement("micra-each-item");
601
- node.style.display = "contents";
602
- node.append(frag);
603
- }
610
+ node = createRowNode(tmpl, state, instance);
604
611
  node.__micraKey = key;
605
612
  keyMap.set(key, node);
606
- const rowScan2 = scanComponent(node);
607
- node.__micraScan = rowScan2;
608
- bindDataOn(rowScan2.on, instance);
609
- bindAtEvents(rowScan2.atEvents, instance);
610
- bindModels(rowScan2.model, instance);
613
+ } else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
614
+ nextNodes.push(node);
615
+ continue;
611
616
  }
612
- const itemState = Object.assign(
613
- Object.create(state),
614
- { item, index, $index: index }
615
- );
617
+ node.__micraItem = item;
618
+ node.__micraIndex = index;
619
+ const itemState = node._itemState;
620
+ itemState.item = item;
621
+ itemState.index = index;
622
+ itemState.$index = index;
616
623
  const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
617
624
  applyDirectives(rowScan, itemState, rawState, instance);
618
625
  nextNodes.push(node);
@@ -623,40 +630,113 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
623
630
  keyMap.delete(key);
624
631
  }
625
632
  }
626
- let cursor = marker;
627
- for (const node of nextNodes) {
628
- if (cursor.nextSibling !== node) parent.insertBefore(node, cursor.nextSibling);
629
- cursor = node;
633
+ const prevList = tmpl.__micraList;
634
+ if (prevList.length === 0) {
635
+ if (nextNodes.length) {
636
+ const frag = document.createDocumentFragment();
637
+ for (const node of nextNodes) frag.append(node);
638
+ marker.after(frag);
639
+ }
640
+ } else {
641
+ let orderChanged = nextNodes.length !== prevList.length;
642
+ if (!orderChanged) {
643
+ for (let i = 0; i < nextNodes.length; i++) {
644
+ if (nextNodes[i] !== prevList[i]) {
645
+ orderChanged = true;
646
+ break;
647
+ }
648
+ }
649
+ }
650
+ if (orderChanged) reorderKeyed(nextNodes, prevList, marker);
630
651
  }
631
652
  tmpl.__micraList = nextNodes;
632
653
  }
633
- function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
634
- tmpl.__micraList.forEach((n) => n.remove());
635
- tmpl.__micraList = [];
636
- const frag = document.createDocumentFragment();
637
- for (const [index, item] of items.entries()) {
638
- const clone = tmpl.content.cloneNode(true);
639
- const itemState = Object.assign(
640
- Object.create(state),
641
- { item, index, $index: index }
642
- );
643
- const fragScan = scanFragment(clone);
644
- applyDirectives(fragScan, itemState, rawState, instance);
645
- bindDataOn(fragScan.on, instance);
646
- bindAtEvents(fragScan.atEvents, instance);
647
- bindModels(fragScan.model, instance);
648
- const nodes = Array.from(clone.childNodes);
649
- nodes.forEach((n) => {
650
- n.__micraEach = true;
651
- frag.append(n);
652
- });
653
- tmpl.__micraList.push(...nodes);
654
+ function reorderKeyed(nextNodes, prevList, marker) {
655
+ const prevPos = /* @__PURE__ */ new Map();
656
+ for (let i = 0; i < prevList.length; i++) prevPos.set(prevList[i], i);
657
+ const n = nextNodes.length;
658
+ const tails = [];
659
+ const tailIdx = [];
660
+ const prev = new Array(n).fill(-1);
661
+ for (let i = 0; i < n; i++) {
662
+ const p = prevPos.get(nextNodes[i]);
663
+ if (p === void 0) continue;
664
+ let lo = 0, hi = tails.length;
665
+ while (lo < hi) {
666
+ const m = lo + hi >> 1;
667
+ tails[m] < p ? lo = m + 1 : hi = m;
668
+ }
669
+ if (lo > 0) prev[i] = tailIdx[lo - 1];
670
+ tails[lo] = p;
671
+ tailIdx[lo] = i;
672
+ }
673
+ const stable = /* @__PURE__ */ new Set();
674
+ let idx = tailIdx[tails.length - 1];
675
+ while (idx >= 0) {
676
+ stable.add(idx);
677
+ idx = prev[idx];
678
+ }
679
+ let anchor = marker;
680
+ for (let i = 0; i < n; i++) {
681
+ const node = nextNodes[i];
682
+ if (stable.has(i)) {
683
+ anchor = node;
684
+ continue;
685
+ }
686
+ anchor.after(node);
687
+ anchor = node;
688
+ }
689
+ }
690
+ function renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged) {
691
+ const prevList = tmpl.__micraList;
692
+ const prevLen = prevList.length;
693
+ const nextLen = items.length;
694
+ const reuseLen = nextLen < prevLen ? nextLen : prevLen;
695
+ const nextList = new Array(nextLen);
696
+ for (let i = 0; i < reuseLen; i++) {
697
+ const node = prevList[i];
698
+ const item = items[i];
699
+ if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === i) {
700
+ nextList[i] = node;
701
+ continue;
702
+ }
703
+ node.__micraItem = item;
704
+ node.__micraIndex = i;
705
+ const itemState = node._itemState;
706
+ itemState.item = item;
707
+ itemState.index = i;
708
+ itemState.$index = i;
709
+ applyDirectives(node.__micraScan, itemState, rawState, instance);
710
+ nextList[i] = node;
711
+ }
712
+ for (let i = nextLen; i < prevLen; i++) {
713
+ prevList[i].remove();
714
+ }
715
+ if (nextLen > prevLen) {
716
+ const frag = document.createDocumentFragment();
717
+ for (let i = prevLen; i < nextLen; i++) {
718
+ const node = createRowNode(tmpl, state, instance);
719
+ const item = items[i];
720
+ const itemState = node._itemState;
721
+ itemState.item = item;
722
+ itemState.index = i;
723
+ itemState.$index = i;
724
+ node.__micraEach = true;
725
+ node.__micraItem = item;
726
+ node.__micraIndex = i;
727
+ applyDirectives(node.__micraScan, itemState, rawState, instance);
728
+ nextList[i] = node;
729
+ frag.append(node);
730
+ }
731
+ const anchor = prevLen > 0 ? nextList[prevLen - 1] : marker;
732
+ anchor.after(frag);
654
733
  }
655
- parent.insertBefore(frag, marker.nextSibling);
734
+ tmpl.__micraList = nextList;
656
735
  }
657
736
 
658
737
  // src/dom/refs.ts
659
738
  function collectRefs(els, instance) {
739
+ if (!els.length) return;
660
740
  instance.refs = {};
661
741
  for (const el of els) {
662
742
  const name = el.dataset["ref"];
@@ -699,8 +779,12 @@ function mount(selector, definition) {
699
779
  return unsub;
700
780
  };
701
781
  let isRendering = false;
782
+ let _triggerKey = null;
702
783
  const schedule = createScheduler(() => instance.render());
703
- instance.state = createReactiveState(rawState, schedule);
784
+ instance.state = createReactiveState(rawState, schedule, (key) => {
785
+ if (_triggerKey === null) _triggerKey = key;
786
+ else if (_triggerKey !== key) _triggerKey = "MULTIPLE";
787
+ });
704
788
  const boundMethods = /* @__PURE__ */ new Map();
705
789
  const exprState = new Proxy(rawState, {
706
790
  get(target, key) {
@@ -724,6 +808,8 @@ function mount(selector, definition) {
724
808
  instance.render = function() {
725
809
  var _a2;
726
810
  if (instance.__micraDestroyed) return;
811
+ const triggerKey = _triggerKey;
812
+ _triggerKey = null;
727
813
  if (isRendering) {
728
814
  if (!warnedReentry) {
729
815
  warn(
@@ -738,7 +824,7 @@ function mount(selector, definition) {
738
824
  const mRoot2 = root;
739
825
  const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
740
826
  applyDirectives(scan, exprState, rawState, instance);
741
- renderList(scan.each, exprState, rawState, instance);
827
+ renderList(scan.each, exprState, rawState, instance, triggerKey);
742
828
  bindDataOn(scan.on, instance);
743
829
  bindAtEvents(scan.atEvents, instance);
744
830
  bindModels(scan.model, instance);