micra.js 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/micra.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v2.1.0 — https://github.com/micra-js/micra — MIT */
1
+ /* Micra.js v2.2.1 — https://github.com/micra-js/micra — MIT */
2
2
 
3
3
  // src/utils/fetch.ts
4
4
  function getCSRF() {
@@ -141,27 +141,32 @@ function safeStateHas(state, key) {
141
141
  return false;
142
142
  }
143
143
  function evalExpr(expr, state) {
144
- if (SIMPLE_PATH.test(expr)) {
145
- const parts = expr.split(".");
146
- if (!safeStateHas(state, parts[0])) return void 0;
147
- return parts.reduce(
144
+ let cached = exprCache.get(expr);
145
+ if (!cached) {
146
+ if (SIMPLE_PATH.test(expr)) {
147
+ cached = { kind: "path", parts: expr.split(".") };
148
+ } else {
149
+ try {
150
+ cached = {
151
+ kind: "fn",
152
+ fn: new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
153
+ };
154
+ } catch {
155
+ warn(`invalid expression "${expr}"`);
156
+ cached = { kind: "fn", fn: () => void 0 };
157
+ }
158
+ }
159
+ exprCache.set(expr, cached);
160
+ }
161
+ if (cached.kind === "path") {
162
+ if (!safeStateHas(state, cached.parts[0])) return void 0;
163
+ return cached.parts.reduce(
148
164
  (obj, key) => obj != null ? obj[key] : void 0,
149
165
  state
150
166
  );
151
167
  }
152
- if (!exprCache.has(expr)) {
153
- try {
154
- exprCache.set(
155
- expr,
156
- new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
157
- );
158
- } catch {
159
- warn(`invalid expression "${expr}"`);
160
- exprCache.set(expr, () => void 0);
161
- }
162
- }
163
168
  try {
164
- return exprCache.get(expr)(safeStateWrap(state), SAFE_OUTER);
169
+ return cached.fn(safeStateWrap(state), SAFE_OUTER);
165
170
  } catch (e) {
166
171
  if (!warnedRuntime.has(expr)) {
167
172
  warnedRuntime.add(expr);
@@ -199,11 +204,12 @@ function emit(event, payload) {
199
204
  }
200
205
 
201
206
  // src/core/reactive.ts
202
- function createReactiveState(obj, schedule) {
207
+ function createReactiveState(obj, schedule, onKey) {
203
208
  return new Proxy(obj, {
204
209
  set(target, key, value) {
205
210
  ;
206
211
  target[key] = value;
212
+ onKey == null ? void 0 : onKey(key);
207
213
  schedule();
208
214
  return true;
209
215
  }
@@ -221,27 +227,6 @@ function createScheduler(render) {
221
227
  };
222
228
  }
223
229
 
224
- // src/dom/query.ts
225
- function queryAll(root, sel) {
226
- return Array.from(root.querySelectorAll(sel));
227
- }
228
- function queryOwn(root, attr) {
229
- return filterOwn(root, queryAll(root, `[${attr}]`));
230
- }
231
- function queryOwnAll(root, sel) {
232
- return filterOwn(root, queryAll(root, sel));
233
- }
234
- function filterOwn(root, els) {
235
- return els.filter((el) => {
236
- let node = el.parentElement;
237
- while (node && node !== root) {
238
- if (node.hasAttribute("data-component")) return false;
239
- node = node.parentElement;
240
- }
241
- return true;
242
- });
243
- }
244
-
245
230
  // src/dom/directives.ts
246
231
  function applyText(el, expr, state) {
247
232
  var _a;
@@ -250,7 +235,8 @@ function applyText(el, expr, state) {
250
235
  }
251
236
  function applyHtml(el, expr, state) {
252
237
  var _a;
253
- el.innerHTML = String((_a = evalExpr(expr, state)) != null ? _a : "");
238
+ const html = String((_a = evalExpr(expr, state)) != null ? _a : "");
239
+ if (el.innerHTML !== html) el.innerHTML = html;
254
240
  }
255
241
  function applyIf(binding, state) {
256
242
  const el = binding.el;
@@ -267,7 +253,9 @@ function applyIf(binding, state) {
267
253
  }
268
254
  }
269
255
  function applyShow(el, expr, state) {
270
- el.style.display = evalExpr(expr, state) ? "" : "none";
256
+ const desired = evalExpr(expr, state) ? "" : "none";
257
+ const htmlEl = el;
258
+ if (htmlEl.style.display !== desired) htmlEl.style.display = desired;
271
259
  }
272
260
  function applyBind(el, pairs, state) {
273
261
  for (const [attr, valExpr] of pairs) {
@@ -295,92 +283,37 @@ function applyClass(el, pairs, state) {
295
283
  el.classList.toggle(cls, Boolean(evalExpr(valExpr, state)));
296
284
  }
297
285
  }
298
- function parsePairs(expr) {
299
- const out = [];
300
- for (const part of expr.split(",")) {
301
- const colonIdx = part.indexOf(":");
302
- if (colonIdx === -1) continue;
303
- const left = part.slice(0, colonIdx).trim();
304
- const right = part.slice(colonIdx + 1).trim();
305
- if (!left) continue;
306
- out.push([left, right]);
307
- }
308
- return out;
309
- }
310
286
  function applyModel(el, key, rawState) {
311
287
  const html = el;
312
288
  const stateVal = rawState[key];
313
289
  const desired = stateVal == null ? "" : String(stateVal);
314
290
  if (html.value !== desired) html.value = desired;
315
291
  }
316
- function buildCache(root) {
317
- const pick = (attr) => {
318
- var _a;
319
- const els = queryOwn(root, attr);
320
- if ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, attr)) els.unshift(root);
321
- return els.filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
322
- };
323
- const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
324
- return {
325
- text: pick("data-text"),
326
- html: pick("data-html"),
327
- if: pick("data-if"),
328
- show: pick("data-show"),
329
- bind: pickPairs("data-bind"),
330
- model: pick("data-model"),
331
- class: pickPairs("data-class")
332
- };
333
- }
334
- function applyDirectives(root, state, rawState, _instance) {
335
- if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
336
- applyFromList(buildFragmentList(root), state, rawState);
337
- return;
338
- }
339
- const el = root;
340
- if (!el.__micraCache) el.__micraCache = buildCache(el);
341
- applyFromList(el.__micraCache, state, rawState);
342
- }
343
- function applyFromList(cache, state, rawState) {
344
- cache.if.forEach((b) => applyIf(b, state));
345
- cache.text.forEach((b) => applyText(b.el, b.expr, state));
346
- cache.html.forEach((b) => applyHtml(b.el, b.expr, state));
347
- cache.show.forEach((b) => applyShow(b.el, b.expr, state));
348
- cache.bind.forEach((b) => applyBind(b.el, b.pairs, state));
349
- cache.model.forEach((b) => applyModel(b.el, b.expr.trim(), rawState));
350
- cache.class.forEach((b) => applyClass(b.el, b.pairs, state));
351
- }
352
- function buildFragmentList(frag) {
353
- const pick = (attr) => queryAll(frag, `[${attr}]`).filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
354
- const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
355
- return {
356
- text: pick("data-text"),
357
- html: pick("data-html"),
358
- if: pick("data-if"),
359
- show: pick("data-show"),
360
- bind: pickPairs("data-bind"),
361
- model: pick("data-model"),
362
- class: pickPairs("data-class")
363
- };
364
- }
365
- function validateDirectives(root) {
366
- var _a, _b;
367
- queryOwn(root, "data-each").forEach((el) => {
292
+ function applyDirectives(scan, state, rawState, _instance) {
293
+ for (const b of scan.if) applyIf(b, state);
294
+ for (const b of scan.text) applyText(b.el, b.expr, state);
295
+ for (const b of scan.html) applyHtml(b.el, b.expr, state);
296
+ for (const b of scan.show) applyShow(b.el, b.expr, state);
297
+ for (const b of scan.bind) applyBind(b.el, b.pairs, state);
298
+ for (const b of scan.model) applyModel(b.el, b.expr.trim(), rawState);
299
+ for (const b of scan.class) applyClass(b.el, b.pairs, state);
300
+ }
301
+ function validateDirectives(scan) {
302
+ for (const el of scan.each) {
368
303
  const tmpl = el;
369
304
  if (!el.hasAttribute("data-key") && !tmpl.__micraNoKeyWarned) {
370
305
  tmpl.__micraNoKeyWarned = true;
371
- warn(`data-each="${el.getAttribute("data-each")}" has no data-key \u2014 keyed diff disabled. Add data-key="id" for better performance.`);
306
+ warn(
307
+ `data-each="${el.getAttribute("data-each")}" has no data-key \u2014 keyed diff disabled. Add data-key="id" for better performance.`
308
+ );
372
309
  }
373
- });
374
- const bindEls = queryOwn(root, "data-bind");
375
- if (((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-bind")) && !bindEls.includes(root)) bindEls.unshift(root);
376
- for (const el of bindEls) {
377
- const spec = (_b = el.getAttribute("data-bind")) != null ? _b : "";
378
- const hasClassBind = spec.split(",").some((p) => {
379
- var _a2;
380
- return ((_a2 = p.trim().split(":")[0]) == null ? void 0 : _a2.trim()) === "class";
381
- });
382
- if (hasClassBind && el.hasAttribute("data-class")) {
383
- warn(`element has both data-bind="class:..." and data-class \u2014 they fight on every render. Use one.`);
310
+ }
311
+ for (const b of scan.bind) {
312
+ const hasClassBind = b.pairs.some((p) => p[0] === "class");
313
+ if (hasClassBind && b.el.hasAttribute("data-class")) {
314
+ warn(
315
+ `element has both data-bind="class:..." and data-class \u2014 they fight on every render. Use one.`
316
+ );
384
317
  }
385
318
  }
386
319
  }
@@ -391,17 +324,13 @@ function track(instance, el, type, fn) {
391
324
  el.addEventListener(type, fn);
392
325
  ((_a = instance.__micraListeners) != null ? _a : instance.__micraListeners = []).push({ el, type, fn });
393
326
  }
394
- function bindDataOn(root, instance) {
395
- var _a, _b;
396
- const isFragment = root.nodeType === 11;
397
- const els = isFragment ? queryAll(root, "[data-on]") : queryOwn(root, "data-on");
398
- if (!isFragment && ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-on")) && !els.includes(root))
399
- els.unshift(root);
327
+ function bindDataOn(els, instance) {
328
+ var _a;
400
329
  for (const el of els) {
401
330
  const mEl = el;
402
331
  if (mEl.__micraEvents) continue;
403
332
  mEl.__micraEvents = true;
404
- const spec = (_b = mEl.dataset["on"]) != null ? _b : "";
333
+ const spec = (_a = mEl.dataset["on"]) != null ? _a : "";
405
334
  for (const part of spec.split(",")) {
406
335
  const [evSpec, method] = part.trim().split(":");
407
336
  if (!evSpec || !method) continue;
@@ -417,11 +346,8 @@ function bindDataOn(root, instance) {
417
346
  }
418
347
  }
419
348
  }
420
- function bindAtEvents(root, instance) {
421
- const isFragment = root.nodeType === 11;
422
- const all = isFragment ? queryAll(root, "*") : queryOwnAll(root, "*");
423
- if (!isFragment && !all.includes(root)) all.unshift(root);
424
- for (const el of all) {
349
+ function bindAtEvents(els, instance) {
350
+ for (const el of els) {
425
351
  const mEl = el;
426
352
  if (mEl.__micraAtBound) continue;
427
353
  let bound = false;
@@ -442,15 +368,12 @@ function bindAtEvents(root, instance) {
442
368
  if (bound) mEl.__micraAtBound = true;
443
369
  }
444
370
  }
445
- function bindModels(root, instance) {
446
- var _a;
447
- const isFragment = root.nodeType === 11;
448
- const els = isFragment ? queryAll(root, "[data-model]") : queryOwn(root, "data-model");
449
- for (const el of els) {
371
+ function bindModels(bindings, instance) {
372
+ for (const { el, expr } of bindings) {
450
373
  const mEl = el;
451
374
  if (mEl.__micraModel) continue;
452
375
  mEl.__micraModel = true;
453
- const key = (_a = el.dataset["model"]) != null ? _a : "";
376
+ const key = expr.trim();
454
377
  const tag = el.tagName;
455
378
  const inputEl = el;
456
379
  const inputType = inputEl.type;
@@ -471,11 +394,132 @@ function bindModels(root, instance) {
471
394
  }
472
395
  }
473
396
 
397
+ // src/dom/scan.ts
398
+ function emptyScan() {
399
+ return {
400
+ text: [],
401
+ html: [],
402
+ if: [],
403
+ show: [],
404
+ bind: [],
405
+ model: [],
406
+ class: [],
407
+ each: [],
408
+ on: [],
409
+ atEvents: [],
410
+ refs: []
411
+ };
412
+ }
413
+ function parsePairs(expr) {
414
+ const out = [];
415
+ for (const part of expr.split(",")) {
416
+ const colon = part.indexOf(":");
417
+ if (colon === -1) continue;
418
+ const left = part.slice(0, colon).trim();
419
+ const right = part.slice(colon + 1).trim();
420
+ if (!left) continue;
421
+ out.push([left, right]);
422
+ }
423
+ return out;
424
+ }
425
+ function classify(el, scan) {
426
+ if (el.tagName === "TEMPLATE") {
427
+ if (el.hasAttribute("data-each")) scan.each.push(el);
428
+ return;
429
+ }
430
+ const attrs = el.attributes;
431
+ let atEventSeen = false;
432
+ for (let i = 0; i < attrs.length; i++) {
433
+ const a = attrs[i];
434
+ const name = a.name;
435
+ const first = name.charCodeAt(0);
436
+ if (first === 64) {
437
+ if (!atEventSeen) {
438
+ scan.atEvents.push(el);
439
+ atEventSeen = true;
440
+ }
441
+ continue;
442
+ }
443
+ if (first === 100 && name.length >= 6 && name.charCodeAt(4) === 45) {
444
+ const rest = name.slice(5);
445
+ switch (rest) {
446
+ case "text":
447
+ scan.text.push({ el, expr: a.value });
448
+ break;
449
+ case "html":
450
+ scan.html.push({ el, expr: a.value });
451
+ break;
452
+ case "if":
453
+ scan.if.push({ el, expr: a.value });
454
+ break;
455
+ case "show":
456
+ scan.show.push({ el, expr: a.value });
457
+ break;
458
+ case "bind": {
459
+ const pairs = parsePairs(a.value);
460
+ scan.bind.push({ el, expr: a.value, pairs });
461
+ break;
462
+ }
463
+ case "model":
464
+ scan.model.push({ el, expr: a.value });
465
+ break;
466
+ case "class": {
467
+ const pairs = parsePairs(a.value);
468
+ scan.class.push({ el, expr: a.value, pairs });
469
+ break;
470
+ }
471
+ case "on":
472
+ scan.on.push(el);
473
+ break;
474
+ case "ref":
475
+ scan.refs.push(el);
476
+ break;
477
+ }
478
+ }
479
+ }
480
+ }
481
+ var NESTED_COMPONENT_FILTER = {
482
+ acceptNode(node) {
483
+ if (node.hasAttribute("data-component"))
484
+ return NodeFilter.FILTER_REJECT;
485
+ return NodeFilter.FILTER_ACCEPT;
486
+ }
487
+ };
488
+ function scanComponent(root) {
489
+ const scan = emptyScan();
490
+ classify(root, scan);
491
+ const walker = document.createTreeWalker(
492
+ root,
493
+ NodeFilter.SHOW_ELEMENT,
494
+ NESTED_COMPONENT_FILTER
495
+ );
496
+ let node = walker.nextNode();
497
+ while (node) {
498
+ classify(node, scan);
499
+ node = walker.nextNode();
500
+ }
501
+ return scan;
502
+ }
503
+ function scanFragment(frag) {
504
+ const scan = emptyScan();
505
+ const walker = document.createTreeWalker(
506
+ frag,
507
+ NodeFilter.SHOW_ELEMENT,
508
+ NESTED_COMPONENT_FILTER
509
+ );
510
+ let node = walker.nextNode();
511
+ while (node) {
512
+ classify(node, scan);
513
+ node = walker.nextNode();
514
+ }
515
+ return scan;
516
+ }
517
+
474
518
  // src/dom/each.ts
475
- function renderList(root, state, rawState, instance) {
476
- queryOwn(root, "data-each").forEach((tmplEl) => {
477
- var _a;
478
- if (tmplEl.tagName !== "TEMPLATE") return;
519
+ function renderList(templates, state, rawState, instance, triggerKey) {
520
+ var _a;
521
+ for (const tmplEl of templates) {
522
+ if (tmplEl.tagName !== "TEMPLATE") continue;
479
523
  const tmpl = tmplEl;
480
524
  const itemsExpr = tmpl.getAttribute("data-each");
481
525
  const keyAttr = (_a = tmpl.getAttribute("data-key")) != null ? _a : null;
@@ -489,22 +533,23 @@ function renderList(root, state, rawState, instance) {
489
533
  }
490
534
  const marker = tmpl.__micraMarker;
491
535
  const keyMap = tmpl.__micraNodes;
492
- const parent = marker.parentNode;
493
- if (!parent) return;
536
+ if (!marker.parentNode) continue;
494
537
  if (!Array.isArray(items)) {
495
538
  tmpl.__micraList.forEach((n) => n.remove());
496
539
  tmpl.__micraList = [];
497
540
  keyMap.clear();
498
- return;
541
+ continue;
499
542
  }
543
+ const canSkipUnchanged = triggerKey !== null && triggerKey !== "MULTIPLE" && triggerKey === itemsExpr;
500
544
  if (keyAttr) {
501
- renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance);
545
+ renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
502
546
  } else {
503
- renderNoKey(tmpl, items, marker, parent, state, rawState, instance);
547
+ renderNoKey(tmpl, items, marker, state, rawState, instance);
504
548
  }
505
- });
549
+ }
506
550
  }
507
- function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
551
+ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged) {
552
+ var _a;
508
553
  const nextKeys = /* @__PURE__ */ new Set();
509
554
  const nextNodes = [];
510
555
  let warnedNullKey = false;
@@ -532,14 +577,24 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
532
577
  }
533
578
  node.__micraKey = key;
534
579
  keyMap.set(key, node);
535
- bindDataOn(node, instance);
536
- bindAtEvents(node, instance);
580
+ const rowScan2 = scanComponent(node);
581
+ node.__micraScan = rowScan2;
582
+ bindDataOn(rowScan2.on, instance);
583
+ bindAtEvents(rowScan2.atEvents, instance);
584
+ bindModels(rowScan2.model, instance);
585
+ node._itemState = Object.create(state);
586
+ } else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
587
+ nextNodes.push(node);
588
+ continue;
537
589
  }
538
- const itemState = Object.assign(
539
- Object.create(state),
540
- { item, index, $index: index }
541
- );
542
- applyDirectives(node, itemState, rawState, instance);
590
+ node.__micraItem = item;
591
+ node.__micraIndex = index;
592
+ const itemState = node._itemState;
593
+ itemState.item = item;
594
+ itemState.index = index;
595
+ itemState.$index = index;
596
+ const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
597
+ applyDirectives(rowScan, itemState, rawState, instance);
543
598
  nextNodes.push(node);
544
599
  }
545
600
  for (const [key, node] of keyMap) {
@@ -548,14 +603,64 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
548
603
  keyMap.delete(key);
549
604
  }
550
605
  }
551
- let cursor = marker;
552
- for (const node of nextNodes) {
553
- if (cursor.nextSibling !== node) parent.insertBefore(node, cursor.nextSibling);
554
- cursor = node;
606
+ const prevList = tmpl.__micraList;
607
+ if (prevList.length === 0) {
608
+ if (nextNodes.length) {
609
+ const frag = document.createDocumentFragment();
610
+ for (const node of nextNodes) frag.append(node);
611
+ marker.after(frag);
612
+ }
613
+ } else {
614
+ let orderChanged = nextNodes.length !== prevList.length;
615
+ if (!orderChanged) {
616
+ for (let i = 0; i < nextNodes.length; i++) {
617
+ if (nextNodes[i] !== prevList[i]) {
618
+ orderChanged = true;
619
+ break;
620
+ }
621
+ }
622
+ }
623
+ if (orderChanged) reorderKeyed(nextNodes, prevList, marker);
555
624
  }
556
625
  tmpl.__micraList = nextNodes;
557
626
  }
558
- function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
627
+ function reorderKeyed(nextNodes, prevList, marker) {
628
+ const prevPos = /* @__PURE__ */ new Map();
629
+ for (let i = 0; i < prevList.length; i++) prevPos.set(prevList[i], i);
630
+ const n = nextNodes.length;
631
+ const tails = [];
632
+ const tailIdx = [];
633
+ const prev = new Array(n).fill(-1);
634
+ for (let i = 0; i < n; i++) {
635
+ const p = prevPos.get(nextNodes[i]);
636
+ if (p === void 0) continue;
637
+ let lo = 0, hi = tails.length;
638
+ while (lo < hi) {
639
+ const m = lo + hi >> 1;
640
+ tails[m] < p ? lo = m + 1 : hi = m;
641
+ }
642
+ if (lo > 0) prev[i] = tailIdx[lo - 1];
643
+ tails[lo] = p;
644
+ tailIdx[lo] = i;
645
+ }
646
+ const stable = /* @__PURE__ */ new Set();
647
+ let idx = tailIdx[tails.length - 1];
648
+ while (idx >= 0) {
649
+ stable.add(idx);
650
+ idx = prev[idx];
651
+ }
652
+ let anchor = marker;
653
+ for (let i = 0; i < n; i++) {
654
+ const node = nextNodes[i];
655
+ if (stable.has(i)) {
656
+ anchor = node;
657
+ continue;
658
+ }
659
+ anchor.after(node);
660
+ anchor = node;
661
+ }
662
+ }
663
+ function renderNoKey(tmpl, items, marker, state, rawState, instance) {
559
664
  tmpl.__micraList.forEach((n) => n.remove());
560
665
  tmpl.__micraList = [];
561
666
  const frag = document.createDocumentFragment();
@@ -565,9 +670,11 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
565
670
  Object.create(state),
566
671
  { item, index, $index: index }
567
672
  );
568
- applyDirectives(clone, itemState, rawState, instance);
569
- bindDataOn(clone, instance);
570
- bindAtEvents(clone, instance);
673
+ const fragScan = scanFragment(clone);
674
+ applyDirectives(fragScan, itemState, rawState, instance);
675
+ bindDataOn(fragScan.on, instance);
676
+ bindAtEvents(fragScan.atEvents, instance);
677
+ bindModels(fragScan.model, instance);
571
678
  const nodes = Array.from(clone.childNodes);
572
679
  nodes.forEach((n) => {
573
680
  n.__micraEach = true;
@@ -575,13 +682,14 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
575
682
  });
576
683
  tmpl.__micraList.push(...nodes);
577
684
  }
578
- parent.insertBefore(frag, marker.nextSibling);
685
+ marker.after(frag);
579
686
  }
580
687
 
581
688
  // src/dom/refs.ts
582
- function collectRefs(root, instance) {
689
+ function collectRefs(els, instance) {
690
+ if (!els.length) return;
583
691
  instance.refs = {};
584
- for (const el of queryOwn(root, "data-ref")) {
692
+ for (const el of els) {
585
693
  const name = el.dataset["ref"];
586
694
  if (name) instance.refs[name] = el;
587
695
  }
@@ -595,10 +703,13 @@ function mount(selector, definition) {
595
703
  warn(`"${selector}" not found`);
596
704
  return null;
597
705
  }
598
- if (_instances.has(root)) return _instances.get(root);
706
+ if (_instances.has(root))
707
+ return _instances.get(root);
599
708
  const rawState = { ...(_a = definition.state) != null ? _a : {} };
600
709
  const instance = { $el: root, refs: {} };
601
- for (const [key, val] of Object.entries(definition)) {
710
+ for (const [key, val] of Object.entries(
711
+ definition
712
+ )) {
602
713
  if (key === "state" || key === "onCreate" || key === "onDestroy") continue;
603
714
  if (typeof val === "function") instance[key] = val;
604
715
  }
@@ -619,8 +730,12 @@ function mount(selector, definition) {
619
730
  return unsub;
620
731
  };
621
732
  let isRendering = false;
733
+ let _triggerKey = null;
622
734
  const schedule = createScheduler(() => instance.render());
623
- instance.state = createReactiveState(rawState, schedule);
735
+ instance.state = createReactiveState(rawState, schedule, (key) => {
736
+ if (_triggerKey === null) _triggerKey = key;
737
+ else if (_triggerKey !== key) _triggerKey = "MULTIPLE";
738
+ });
624
739
  const boundMethods = /* @__PURE__ */ new Map();
625
740
  const exprState = new Proxy(rawState, {
626
741
  get(target, key) {
@@ -642,22 +757,29 @@ function mount(selector, definition) {
642
757
  });
643
758
  let warnedReentry = false;
644
759
  instance.render = function() {
760
+ var _a2;
645
761
  if (instance.__micraDestroyed) return;
762
+ const triggerKey = _triggerKey;
763
+ _triggerKey = null;
646
764
  if (isRendering) {
647
765
  if (!warnedReentry) {
648
- warn("render() re-entry detected \u2014 mutation inside a directive expression is ignored. Move state writes to a method.");
766
+ warn(
767
+ "render() re-entry detected \u2014 mutation inside a directive expression is ignored. Move state writes to a method."
768
+ );
649
769
  warnedReentry = true;
650
770
  }
651
771
  return;
652
772
  }
653
773
  isRendering = true;
654
774
  try {
655
- applyDirectives(root, exprState, rawState, instance);
656
- renderList(root, exprState, rawState, instance);
657
- bindDataOn(root, instance);
658
- bindAtEvents(root, instance);
659
- bindModels(root, instance);
660
- collectRefs(root, instance);
775
+ const mRoot2 = root;
776
+ const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
777
+ applyDirectives(scan, exprState, rawState, instance);
778
+ renderList(scan.each, exprState, rawState, instance, triggerKey);
779
+ bindDataOn(scan.on, instance);
780
+ bindAtEvents(scan.atEvents, instance);
781
+ bindModels(scan.model, instance);
782
+ collectRefs(scan.refs, instance);
661
783
  } finally {
662
784
  isRendering = false;
663
785
  }
@@ -666,14 +788,16 @@ function mount(selector, definition) {
666
788
  var _a2, _b;
667
789
  if (instance.__micraDestroyed) return;
668
790
  instance.__micraDestroyed = true;
669
- (_a2 = instance.__micraListeners) == null ? void 0 : _a2.forEach(({ el, type, fn }) => el.removeEventListener(type, fn));
791
+ (_a2 = instance.__micraListeners) == null ? void 0 : _a2.forEach(
792
+ ({ el, type, fn }) => el.removeEventListener(type, fn)
793
+ );
670
794
  instance.__micraListeners = [];
671
795
  const clearFlags = (el) => {
672
796
  const m = el;
673
797
  delete m.__micraEvents;
674
798
  delete m.__micraAtBound;
675
799
  delete m.__micraModel;
676
- delete m.__micraCache;
800
+ delete m.__micraScan;
677
801
  };
678
802
  clearFlags(root);
679
803
  root.querySelectorAll("*").forEach(clearFlags);
@@ -685,7 +809,8 @@ function mount(selector, definition) {
685
809
  };
686
810
  _instances.set(root, instance);
687
811
  instance.render();
688
- validateDirectives(root);
812
+ const mRoot = root;
813
+ if (mRoot.__micraScan) validateDirectives(mRoot.__micraScan);
689
814
  if (typeof definition.onCreate === "function")
690
815
  Promise.resolve().then(
691
816
  () => definition.onCreate.call(instance)