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 +88 -0
- package/README.md +1 -0
- package/dist/core/bus.d.ts +6 -4
- package/dist/core/reactive.d.ts +1 -1
- package/dist/dom/each.d.ts +11 -7
- package/dist/dom/scan.d.ts +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +172 -86
- package/dist/micra.cjs.js.map +3 -3
- package/dist/micra.esm.js +172 -86
- package/dist/micra.esm.js.map +3 -3
- package/dist/micra.js +172 -86
- package/dist/micra.js.map +3 -3
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +50 -5
- package/llms-full.txt +67 -14
- package/llms.txt +1 -1
- package/package.json +2 -2
- package/src/core/bus.ts +15 -6
- package/src/core/mount.ts +17 -7
- package/src/core/reactive.ts +2 -1
- package/src/dom/directives.ts +5 -2
- package/src/dom/each.ts +190 -59
- package/src/dom/refs.ts +1 -0
- package/src/dom/scan.ts +2 -22
- package/src/index.ts +3 -0
- package/src/types.ts +61 -5
- package/src/utils/expr.ts +34 -21
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
package/dist/core/bus.d.ts
CHANGED
|
@@ -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 {
|
|
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<
|
|
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:
|
|
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:
|
|
34
|
+
export declare function emit<K extends string>(event: K, ...args: EmitArgs<K>): void;
|
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
|
@@ -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:
|
|
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
|
|
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
|
|
23
|
-
* @param state
|
|
24
|
-
* @param rawState
|
|
25
|
-
* @param instance
|
|
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
|
|
31
|
+
export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>, triggerKey: string | null | 'MULTIPLE'): void;
|
package/dist/dom/scan.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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);
|
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
567
|
+
renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
|
|
572
568
|
} else {
|
|
573
|
-
renderNoKey(tmpl, items, marker,
|
|
569
|
+
renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged);
|
|
574
570
|
}
|
|
575
571
|
}
|
|
576
572
|
}
|
|
577
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
607
|
-
node
|
|
608
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
if (
|
|
629
|
-
|
|
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
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
);
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
-
|
|
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);
|