micra.js 2.2.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/CHANGELOG.md +13 -0
- package/dist/core/reactive.d.ts +1 -1
- package/dist/dom/each.d.ts +6 -5
- package/dist/micra.cjs.js +110 -38
- package/dist/micra.cjs.js.map +2 -2
- package/dist/micra.esm.js +110 -38
- package/dist/micra.esm.js.map +2 -2
- package/dist/micra.js +110 -38
- package/dist/micra.js.map +2 -2
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +4 -1
- package/llms-full.txt +14 -14
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/src/core/mount.ts +12 -3
- package/src/core/reactive.ts +2 -1
- package/src/dom/directives.ts +5 -2
- package/src/dom/each.ts +99 -20
- package/src/dom/refs.ts +1 -0
- package/src/types.ts +4 -1
- package/src/utils/expr.ts +34 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ 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.2.1] — 2026-05-28
|
|
8
|
+
|
|
9
|
+
### Performance — batched first list render
|
|
10
|
+
|
|
11
|
+
- **First render of a keyed `data-each` list now inserts in a single DOM
|
|
12
|
+
operation.** `renderKeyed` previously appended each new row with an
|
|
13
|
+
individual `anchor.after(node)` call — N insertions for an N-row list. On the
|
|
14
|
+
initial render (no previous rows to diff against), all freshly-cloned rows are
|
|
15
|
+
now collected into one `DocumentFragment` and inserted with a single
|
|
16
|
+
`marker.after()`, skipping the LIS reorder pass entirely. The update, swap, and
|
|
17
|
+
reorder paths are unchanged.
|
|
18
|
+
- No public-API change. Bundle stays at **5.4 KB gzip**.
|
|
19
|
+
|
|
7
20
|
## [2.2.0] — 2026-05-27
|
|
8
21
|
|
|
9
22
|
### Performance — single-pass DOM scan
|
package/dist/core/reactive.d.ts
CHANGED
|
@@ -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.
|
package/dist/dom/each.d.ts
CHANGED
|
@@ -19,9 +19,10 @@ import type { InternalInstance, StateRecord } from '../types';
|
|
|
19
19
|
* Process all `<template data-each>` elements found by the scanner.
|
|
20
20
|
* Scoped itemState makes `item`, `index`, `$index` available in row expressions.
|
|
21
21
|
*
|
|
22
|
-
* @param templates
|
|
23
|
-
* @param state
|
|
24
|
-
* @param rawState
|
|
25
|
-
* @param instance
|
|
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)
|
|
26
|
+
* @param triggerKey - Which state key triggered this render (null = initial, 'MULTIPLE' = batch)
|
|
26
27
|
*/
|
|
27
|
-
export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S
|
|
28
|
+
export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>, triggerKey: string | null | 'MULTIPLE'): void;
|
package/dist/micra.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Micra.js v2.2.
|
|
1
|
+
/* Micra.js v2.2.1 — 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
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
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
|
|
204
|
+
return cached.fn(safeStateWrap(state), SAFE_OUTER);
|
|
200
205
|
} catch (e) {
|
|
201
206
|
if (!warnedRuntime.has(expr)) {
|
|
202
207
|
warnedRuntime.add(expr);
|
|
@@ -234,11 +239,12 @@ function emit(event, payload) {
|
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
// src/core/reactive.ts
|
|
237
|
-
function createReactiveState(obj, schedule) {
|
|
242
|
+
function createReactiveState(obj, schedule, onKey) {
|
|
238
243
|
return new Proxy(obj, {
|
|
239
244
|
set(target, key, value) {
|
|
240
245
|
;
|
|
241
246
|
target[key] = value;
|
|
247
|
+
onKey == null ? void 0 : onKey(key);
|
|
242
248
|
schedule();
|
|
243
249
|
return true;
|
|
244
250
|
}
|
|
@@ -264,7 +270,8 @@ function applyText(el, expr, state) {
|
|
|
264
270
|
}
|
|
265
271
|
function applyHtml(el, expr, state) {
|
|
266
272
|
var _a;
|
|
267
|
-
|
|
273
|
+
const html = String((_a = evalExpr(expr, state)) != null ? _a : "");
|
|
274
|
+
if (el.innerHTML !== html) el.innerHTML = html;
|
|
268
275
|
}
|
|
269
276
|
function applyIf(binding, state) {
|
|
270
277
|
const el = binding.el;
|
|
@@ -281,7 +288,9 @@ function applyIf(binding, state) {
|
|
|
281
288
|
}
|
|
282
289
|
}
|
|
283
290
|
function applyShow(el, expr, state) {
|
|
284
|
-
|
|
291
|
+
const desired = evalExpr(expr, state) ? "" : "none";
|
|
292
|
+
const htmlEl = el;
|
|
293
|
+
if (htmlEl.style.display !== desired) htmlEl.style.display = desired;
|
|
285
294
|
}
|
|
286
295
|
function applyBind(el, pairs, state) {
|
|
287
296
|
for (const [attr, valExpr] of pairs) {
|
|
@@ -542,7 +551,7 @@ function scanFragment(frag) {
|
|
|
542
551
|
}
|
|
543
552
|
|
|
544
553
|
// src/dom/each.ts
|
|
545
|
-
function renderList(templates, state, rawState, instance) {
|
|
554
|
+
function renderList(templates, state, rawState, instance, triggerKey) {
|
|
546
555
|
var _a;
|
|
547
556
|
for (const tmplEl of templates) {
|
|
548
557
|
if (tmplEl.tagName !== "TEMPLATE") continue;
|
|
@@ -559,22 +568,22 @@ function renderList(templates, state, rawState, instance) {
|
|
|
559
568
|
}
|
|
560
569
|
const marker = tmpl.__micraMarker;
|
|
561
570
|
const keyMap = tmpl.__micraNodes;
|
|
562
|
-
|
|
563
|
-
if (!parent) continue;
|
|
571
|
+
if (!marker.parentNode) continue;
|
|
564
572
|
if (!Array.isArray(items)) {
|
|
565
573
|
tmpl.__micraList.forEach((n) => n.remove());
|
|
566
574
|
tmpl.__micraList = [];
|
|
567
575
|
keyMap.clear();
|
|
568
576
|
continue;
|
|
569
577
|
}
|
|
578
|
+
const canSkipUnchanged = triggerKey !== null && triggerKey !== "MULTIPLE" && triggerKey === itemsExpr;
|
|
570
579
|
if (keyAttr) {
|
|
571
|
-
renderKeyed(tmpl, items, keyAttr, marker, keyMap,
|
|
580
|
+
renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
|
|
572
581
|
} else {
|
|
573
|
-
renderNoKey(tmpl, items, marker,
|
|
582
|
+
renderNoKey(tmpl, items, marker, state, rawState, instance);
|
|
574
583
|
}
|
|
575
584
|
}
|
|
576
585
|
}
|
|
577
|
-
function renderKeyed(tmpl, items, keyAttr, marker, keyMap,
|
|
586
|
+
function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged) {
|
|
578
587
|
var _a;
|
|
579
588
|
const nextKeys = /* @__PURE__ */ new Set();
|
|
580
589
|
const nextNodes = [];
|
|
@@ -608,11 +617,17 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
|
|
|
608
617
|
bindDataOn(rowScan2.on, instance);
|
|
609
618
|
bindAtEvents(rowScan2.atEvents, instance);
|
|
610
619
|
bindModels(rowScan2.model, instance);
|
|
620
|
+
node._itemState = Object.create(state);
|
|
621
|
+
} else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
|
|
622
|
+
nextNodes.push(node);
|
|
623
|
+
continue;
|
|
611
624
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
625
|
+
node.__micraItem = item;
|
|
626
|
+
node.__micraIndex = index;
|
|
627
|
+
const itemState = node._itemState;
|
|
628
|
+
itemState.item = item;
|
|
629
|
+
itemState.index = index;
|
|
630
|
+
itemState.$index = index;
|
|
616
631
|
const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
|
|
617
632
|
applyDirectives(rowScan, itemState, rawState, instance);
|
|
618
633
|
nextNodes.push(node);
|
|
@@ -623,14 +638,64 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
|
|
|
623
638
|
keyMap.delete(key);
|
|
624
639
|
}
|
|
625
640
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
641
|
+
const prevList = tmpl.__micraList;
|
|
642
|
+
if (prevList.length === 0) {
|
|
643
|
+
if (nextNodes.length) {
|
|
644
|
+
const frag = document.createDocumentFragment();
|
|
645
|
+
for (const node of nextNodes) frag.append(node);
|
|
646
|
+
marker.after(frag);
|
|
647
|
+
}
|
|
648
|
+
} else {
|
|
649
|
+
let orderChanged = nextNodes.length !== prevList.length;
|
|
650
|
+
if (!orderChanged) {
|
|
651
|
+
for (let i = 0; i < nextNodes.length; i++) {
|
|
652
|
+
if (nextNodes[i] !== prevList[i]) {
|
|
653
|
+
orderChanged = true;
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (orderChanged) reorderKeyed(nextNodes, prevList, marker);
|
|
630
659
|
}
|
|
631
660
|
tmpl.__micraList = nextNodes;
|
|
632
661
|
}
|
|
633
|
-
function
|
|
662
|
+
function reorderKeyed(nextNodes, prevList, marker) {
|
|
663
|
+
const prevPos = /* @__PURE__ */ new Map();
|
|
664
|
+
for (let i = 0; i < prevList.length; i++) prevPos.set(prevList[i], i);
|
|
665
|
+
const n = nextNodes.length;
|
|
666
|
+
const tails = [];
|
|
667
|
+
const tailIdx = [];
|
|
668
|
+
const prev = new Array(n).fill(-1);
|
|
669
|
+
for (let i = 0; i < n; i++) {
|
|
670
|
+
const p = prevPos.get(nextNodes[i]);
|
|
671
|
+
if (p === void 0) continue;
|
|
672
|
+
let lo = 0, hi = tails.length;
|
|
673
|
+
while (lo < hi) {
|
|
674
|
+
const m = lo + hi >> 1;
|
|
675
|
+
tails[m] < p ? lo = m + 1 : hi = m;
|
|
676
|
+
}
|
|
677
|
+
if (lo > 0) prev[i] = tailIdx[lo - 1];
|
|
678
|
+
tails[lo] = p;
|
|
679
|
+
tailIdx[lo] = i;
|
|
680
|
+
}
|
|
681
|
+
const stable = /* @__PURE__ */ new Set();
|
|
682
|
+
let idx = tailIdx[tails.length - 1];
|
|
683
|
+
while (idx >= 0) {
|
|
684
|
+
stable.add(idx);
|
|
685
|
+
idx = prev[idx];
|
|
686
|
+
}
|
|
687
|
+
let anchor = marker;
|
|
688
|
+
for (let i = 0; i < n; i++) {
|
|
689
|
+
const node = nextNodes[i];
|
|
690
|
+
if (stable.has(i)) {
|
|
691
|
+
anchor = node;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
anchor.after(node);
|
|
695
|
+
anchor = node;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function renderNoKey(tmpl, items, marker, state, rawState, instance) {
|
|
634
699
|
tmpl.__micraList.forEach((n) => n.remove());
|
|
635
700
|
tmpl.__micraList = [];
|
|
636
701
|
const frag = document.createDocumentFragment();
|
|
@@ -652,11 +717,12 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
|
|
|
652
717
|
});
|
|
653
718
|
tmpl.__micraList.push(...nodes);
|
|
654
719
|
}
|
|
655
|
-
|
|
720
|
+
marker.after(frag);
|
|
656
721
|
}
|
|
657
722
|
|
|
658
723
|
// src/dom/refs.ts
|
|
659
724
|
function collectRefs(els, instance) {
|
|
725
|
+
if (!els.length) return;
|
|
660
726
|
instance.refs = {};
|
|
661
727
|
for (const el of els) {
|
|
662
728
|
const name = el.dataset["ref"];
|
|
@@ -699,8 +765,12 @@ function mount(selector, definition) {
|
|
|
699
765
|
return unsub;
|
|
700
766
|
};
|
|
701
767
|
let isRendering = false;
|
|
768
|
+
let _triggerKey = null;
|
|
702
769
|
const schedule = createScheduler(() => instance.render());
|
|
703
|
-
instance.state = createReactiveState(rawState, schedule)
|
|
770
|
+
instance.state = createReactiveState(rawState, schedule, (key) => {
|
|
771
|
+
if (_triggerKey === null) _triggerKey = key;
|
|
772
|
+
else if (_triggerKey !== key) _triggerKey = "MULTIPLE";
|
|
773
|
+
});
|
|
704
774
|
const boundMethods = /* @__PURE__ */ new Map();
|
|
705
775
|
const exprState = new Proxy(rawState, {
|
|
706
776
|
get(target, key) {
|
|
@@ -724,6 +794,8 @@ function mount(selector, definition) {
|
|
|
724
794
|
instance.render = function() {
|
|
725
795
|
var _a2;
|
|
726
796
|
if (instance.__micraDestroyed) return;
|
|
797
|
+
const triggerKey = _triggerKey;
|
|
798
|
+
_triggerKey = null;
|
|
727
799
|
if (isRendering) {
|
|
728
800
|
if (!warnedReentry) {
|
|
729
801
|
warn(
|
|
@@ -738,7 +810,7 @@ function mount(selector, definition) {
|
|
|
738
810
|
const mRoot2 = root;
|
|
739
811
|
const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
|
|
740
812
|
applyDirectives(scan, exprState, rawState, instance);
|
|
741
|
-
renderList(scan.each, exprState, rawState, instance);
|
|
813
|
+
renderList(scan.each, exprState, rawState, instance, triggerKey);
|
|
742
814
|
bindDataOn(scan.on, instance);
|
|
743
815
|
bindAtEvents(scan.atEvents, instance);
|
|
744
816
|
bindModels(scan.model, instance);
|