micra.js 2.2.1 → 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 +75 -0
- package/README.md +1 -0
- package/dist/core/bus.d.ts +6 -4
- package/dist/dom/each.d.ts +5 -2
- package/dist/dom/scan.d.ts +2 -6
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +67 -53
- package/dist/micra.cjs.js.map +3 -3
- package/dist/micra.esm.js +67 -53
- package/dist/micra.esm.js.map +3 -3
- package/dist/micra.js +67 -53
- package/dist/micra.js.map +3 -3
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +46 -4
- 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 +5 -4
- package/src/dom/each.ts +97 -45
- package/src/dom/scan.ts +2 -22
- package/src/index.ts +3 -0
- package/src/types.ts +57 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,81 @@ 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
|
+
|
|
7
82
|
## [2.2.1] — 2026-05-28
|
|
8
83
|
|
|
9
84
|
### Performance — batched first list render
|
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/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,7 +13,9 @@
|
|
|
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
|
/**
|
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;
|
|
@@ -227,8 +227,9 @@ function off(event, handler) {
|
|
|
227
227
|
set.delete(handler);
|
|
228
228
|
if (set.size === 0) _bus.delete(event);
|
|
229
229
|
}
|
|
230
|
-
function emit(event,
|
|
230
|
+
function emit(event, ...args) {
|
|
231
231
|
var _a;
|
|
232
|
+
const payload = args[0];
|
|
232
233
|
(_a = _bus.get(event)) == null ? void 0 : _a.forEach((h) => {
|
|
233
234
|
try {
|
|
234
235
|
h(payload);
|
|
@@ -535,20 +536,6 @@ function scanComponent(root) {
|
|
|
535
536
|
}
|
|
536
537
|
return scan;
|
|
537
538
|
}
|
|
538
|
-
function scanFragment(frag) {
|
|
539
|
-
const scan = emptyScan();
|
|
540
|
-
const walker = document.createTreeWalker(
|
|
541
|
-
frag,
|
|
542
|
-
NodeFilter.SHOW_ELEMENT,
|
|
543
|
-
NESTED_COMPONENT_FILTER
|
|
544
|
-
);
|
|
545
|
-
let node = walker.nextNode();
|
|
546
|
-
while (node) {
|
|
547
|
-
classify(node, scan);
|
|
548
|
-
node = walker.nextNode();
|
|
549
|
-
}
|
|
550
|
-
return scan;
|
|
551
|
-
}
|
|
552
539
|
|
|
553
540
|
// src/dom/each.ts
|
|
554
541
|
function renderList(templates, state, rawState, instance, triggerKey) {
|
|
@@ -579,10 +566,28 @@ function renderList(templates, state, rawState, instance, triggerKey) {
|
|
|
579
566
|
if (keyAttr) {
|
|
580
567
|
renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged);
|
|
581
568
|
} else {
|
|
582
|
-
renderNoKey(tmpl, items, marker, state, rawState, instance);
|
|
569
|
+
renderNoKey(tmpl, items, marker, state, rawState, instance, canSkipUnchanged);
|
|
583
570
|
}
|
|
584
571
|
}
|
|
585
572
|
}
|
|
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
|
+
}
|
|
586
591
|
function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, instance, canSkipUnchanged) {
|
|
587
592
|
var _a;
|
|
588
593
|
const nextKeys = /* @__PURE__ */ new Set();
|
|
@@ -602,22 +607,9 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, state, rawState, inst
|
|
|
602
607
|
nextKeys.add(key);
|
|
603
608
|
let node = keyMap.get(key);
|
|
604
609
|
if (!node) {
|
|
605
|
-
|
|
606
|
-
if (frag.childNodes.length === 1) {
|
|
607
|
-
node = frag.firstElementChild;
|
|
608
|
-
} else {
|
|
609
|
-
node = document.createElement("micra-each-item");
|
|
610
|
-
node.style.display = "contents";
|
|
611
|
-
node.append(frag);
|
|
612
|
-
}
|
|
610
|
+
node = createRowNode(tmpl, state, instance);
|
|
613
611
|
node.__micraKey = key;
|
|
614
612
|
keyMap.set(key, node);
|
|
615
|
-
const rowScan2 = scanComponent(node);
|
|
616
|
-
node.__micraScan = rowScan2;
|
|
617
|
-
bindDataOn(rowScan2.on, instance);
|
|
618
|
-
bindAtEvents(rowScan2.atEvents, instance);
|
|
619
|
-
bindModels(rowScan2.model, instance);
|
|
620
|
-
node._itemState = Object.create(state);
|
|
621
613
|
} else if (canSkipUnchanged && node.__micraItem === item && node.__micraIndex === index) {
|
|
622
614
|
nextNodes.push(node);
|
|
623
615
|
continue;
|
|
@@ -695,29 +687,51 @@ function reorderKeyed(nextNodes, prevList, marker) {
|
|
|
695
687
|
anchor = node;
|
|
696
688
|
}
|
|
697
689
|
}
|
|
698
|
-
function renderNoKey(tmpl, items, marker, state, rawState, instance) {
|
|
699
|
-
tmpl.__micraList
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
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);
|
|
719
733
|
}
|
|
720
|
-
|
|
734
|
+
tmpl.__micraList = nextList;
|
|
721
735
|
}
|
|
722
736
|
|
|
723
737
|
// src/dom/refs.ts
|