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/CHANGELOG.md +244 -0
- package/README.md +39 -14
- package/dist/core/mount.d.ts +8 -2
- package/dist/core/reactive.d.ts +1 -1
- package/dist/core/registry.d.ts +13 -4
- package/dist/dom/directives.d.ts +11 -15
- package/dist/dom/each.d.ts +10 -7
- package/dist/dom/events.d.ts +15 -5
- package/dist/dom/refs.d.ts +5 -5
- package/dist/dom/scan.d.ts +34 -0
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +302 -177
- package/dist/micra.cjs.js.map +4 -4
- package/dist/micra.esm.js +302 -177
- package/dist/micra.esm.js.map +4 -4
- package/dist/micra.js +302 -177
- package/dist/micra.js.map +4 -4
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +67 -22
- package/llms-full.txt +600 -0
- package/llms.txt +148 -0
- package/package.json +11 -3
- package/src/core/mount.ts +136 -99
- package/src/core/reactive.ts +2 -1
- package/src/core/registry.ts +19 -9
- package/src/dom/directives.ts +39 -122
- package/src/dom/each.ts +133 -37
- package/src/dom/events.ts +23 -31
- package/src/dom/refs.ts +7 -7
- package/src/dom/scan.ts +189 -0
- package/src/index.ts +2 -0
- package/src/types.ts +80 -22
- package/src/utils/expr.ts +34 -21
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to Micra.js will be documented in this file. Format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows
|
|
5
|
+
[SemVer](https://semver.org/spec/v2.0.0.html).
|
|
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
|
+
|
|
20
|
+
## [2.2.0] — 2026-05-27
|
|
21
|
+
|
|
22
|
+
### Performance — single-pass DOM scan
|
|
23
|
+
|
|
24
|
+
- **Mount cost cut roughly in half.** Internal `applyDirectives`,
|
|
25
|
+
`bindDataOn`, `bindAtEvents`, `bindModels`, `collectRefs`, and
|
|
26
|
+
`renderList` used to walk the DOM 10+ times per render via separate
|
|
27
|
+
`querySelectorAll` calls. They now consume a single pre-computed
|
|
28
|
+
`ScanIndex` built by one `TreeWalker` traversal. The walker
|
|
29
|
+
`FILTER_REJECT`s subtrees rooted at nested `[data-component]` — those
|
|
30
|
+
subtrees aren't even visited.
|
|
31
|
+
- Cross-library benchmark numbers on Firefox 150 / Mac (median of 7 runs):
|
|
32
|
+
|
|
33
|
+
| Scenario | Before | After | Vs Alpine.js | Vs petite-vue |
|
|
34
|
+
| ------------------------- | ------: | --------: | ------------: | ------------: |
|
|
35
|
+
| Mount 100 components | 10.8 ms | **5.6 ms**| × 4.9 faster | × 3.6 faster |
|
|
36
|
+
| Mount 1000 components |128.3 ms |**65.4 ms**| × 7.0 faster | × 2.4 faster |
|
|
37
|
+
| Update 5 of 1000 rows | — | **1 ms** | × 886 faster | × 1002 faster |
|
|
38
|
+
| 10,000 state writes | — | **1 ms** | × 980 faster | × 983 faster |
|
|
39
|
+
| First render 1000 keyed | — | **12 ms** | × 79 faster | × 82 faster |
|
|
40
|
+
| Swap first ↔ last of 1000 | — | **7 ms** | × 131 faster | × 143 faster |
|
|
41
|
+
|
|
42
|
+
Bundle stays at **5.0 KB gzip** — the rewrite removed code, not added it.
|
|
43
|
+
|
|
44
|
+
### TypeScript — full inference from your component literal
|
|
45
|
+
|
|
46
|
+
- **Method-level type inference.** Both `S` (state shape) and `M`
|
|
47
|
+
(method set) are now inferred from the object literal passed to
|
|
48
|
+
`Micra.define` / `Micra.mount`. Inside method bodies and lifecycle
|
|
49
|
+
hooks **both** `this.state.X` and `this.someMethod()` are fully typed:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
Micra.define('counter', {
|
|
53
|
+
state: { count: 0 },
|
|
54
|
+
inc() {
|
|
55
|
+
this.state.count++ // ✓ number
|
|
56
|
+
this.dec() // ✓ inferred sibling method
|
|
57
|
+
// this.foo() // ❌ Property 'foo' does not exist
|
|
58
|
+
},
|
|
59
|
+
dec() { this.state.count-- },
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Public `ComponentInstance<S, M>` and `ComponentDefinition<S, M>` now
|
|
64
|
+
take a second generic parameter for methods. `mount()` returns a fully
|
|
65
|
+
typed instance — `inst.inc()` and `inst.state.count` are both checked
|
|
66
|
+
at the call site.
|
|
67
|
+
- New `ComponentMethods` and `ComponentBuiltins` types exported for
|
|
68
|
+
advanced typing.
|
|
69
|
+
|
|
70
|
+
### Breaking — internal only
|
|
71
|
+
|
|
72
|
+
- The internal directive scan format changed (`DirectiveCache` →
|
|
73
|
+
`ScanIndex`). Internal-only — no public-API change. If you reached
|
|
74
|
+
into internals via deep imports, switch to consuming
|
|
75
|
+
`el.__micraScan` instead of `el.__micraCache`.
|
|
76
|
+
|
|
77
|
+
## [2.1.0] — 2026-05-25
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- **`this.fetch(url, { signal })` now forwards `AbortSignal` to the native
|
|
82
|
+
`fetch()`.** Previously the `signal` option was treated as any other
|
|
83
|
+
GET-option and serialized into the URL as `&signal=[object AbortSignal]`,
|
|
84
|
+
while never reaching the underlying request — so abort silently did
|
|
85
|
+
nothing. After this release:
|
|
86
|
+
- `signal` passes through verbatim to native `fetch()`.
|
|
87
|
+
- `signal` is excluded from the GET-querystring serialization loop.
|
|
88
|
+
- `AbortController#abort()` rejects the in-flight request with an
|
|
89
|
+
`AbortError`, matching native semantics.
|
|
90
|
+
- Enables the canonical search-debounce pattern (drop a stale request when
|
|
91
|
+
a fresher query arrives) without dropping to native `fetch` manually.
|
|
92
|
+
- Migration: none — purely additive, the previous URL-serialization
|
|
93
|
+
behaviour was a bug.
|
|
94
|
+
|
|
95
|
+
### Tests
|
|
96
|
+
|
|
97
|
+
- 76 new tests for the components and recipes shipped on the docs site
|
|
98
|
+
(14 components + 6 recipes). Total suite: 235 tests across 13 files.
|
|
99
|
+
|
|
100
|
+
## [2.0.0] — 2026-05-24
|
|
101
|
+
|
|
102
|
+
### Breaking
|
|
103
|
+
|
|
104
|
+
- **`data-if` now truly unmounts the element from the DOM.** Previously
|
|
105
|
+
`data-if` and `data-show` were aliases — both toggled `style.display`.
|
|
106
|
+
Now `data-if` detaches the element (replacing it with a Comment placeholder)
|
|
107
|
+
when falsy and re-inserts it when truthy. `data-show` keeps the old
|
|
108
|
+
`style.display` behaviour and is the way to express cheap visibility
|
|
109
|
+
toggling.
|
|
110
|
+
- Side effect: `this.refs.X` is `undefined` while the element is detached.
|
|
111
|
+
- DOM listeners on the detached node survive — re-insert preserves identity.
|
|
112
|
+
- `<template data-each>` inside a `data-if=false` subtree is suspended and
|
|
113
|
+
re-renders cleanly when the ancestor returns.
|
|
114
|
+
- **Migration:** if you relied on `data-if` keeping the element in the DOM
|
|
115
|
+
(e.g. you were reading `this.refs.X` while hidden, or animating
|
|
116
|
+
`display` transitions), replace those `data-if` attributes with
|
|
117
|
+
`data-show`.
|
|
118
|
+
|
|
119
|
+
### Fixed
|
|
120
|
+
|
|
121
|
+
- **`@event` shorthand no longer crosses nested `data-component` boundaries.**
|
|
122
|
+
`bindAtEvents` previously walked all descendants via `queryAll('*')`,
|
|
123
|
+
attaching parent-component handlers to elements owned by a nested child
|
|
124
|
+
component. It now uses `queryOwnAll` like `data-on`/`data-model` already do.
|
|
125
|
+
- **`this.fetch(url, { method: 'POST' })` without a `body` no longer sends
|
|
126
|
+
the options object as the body.** Previously `body` was set to
|
|
127
|
+
`JSON.stringify(options)` (which serialized `{"method":"POST"}` to the
|
|
128
|
+
server). Now the body is omitted unless `options.body` is provided.
|
|
129
|
+
|
|
130
|
+
### Added
|
|
131
|
+
|
|
132
|
+
- **`queryOwnAll(root, selector)`** in `src/dom/query.ts` — selector variant
|
|
133
|
+
of `queryOwn` for cases where there is no attribute to query by (e.g.
|
|
134
|
+
scanning `*` for `@`-prefixed attribute names).
|
|
135
|
+
- **Recipe: `docs/recipes/sse.md`** — server-sent events pattern using
|
|
136
|
+
`onCreate` + native `EventSource` + `onDestroy` cleanup. No new library
|
|
137
|
+
surface; just the canonical pattern for live data on top of Micra.
|
|
138
|
+
|
|
139
|
+
### Docs
|
|
140
|
+
|
|
141
|
+
- `docs/directives.md` — full split between `data-if` (unmount) and
|
|
142
|
+
`data-show` (display).
|
|
143
|
+
- `docs/llm-guide.md`, `PROMPT.md`, `llms.txt`, `llms-full.txt` — updated
|
|
144
|
+
the directive table and added a "when to pick which" rule for AI agents.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## [1.1.0] — 2026-05-24
|
|
149
|
+
|
|
150
|
+
### Security
|
|
151
|
+
|
|
152
|
+
- **Directive expressions now shadow non-whitelisted globals.** Identifiers in
|
|
153
|
+
`data-text`, `data-if`, `data-bind`, etc. resolve to state keys, instance
|
|
154
|
+
methods, or one of the whitelisted globals: `Math`, `JSON`, `Date`, `String`,
|
|
155
|
+
`Number`, `Boolean`, `Array`, `Object`, `parseInt`, `parseFloat`, `isNaN`,
|
|
156
|
+
`isFinite`, `NaN`, `Infinity`, `undefined`. Everything else (`window`,
|
|
157
|
+
`document`, `fetch`, `eval`, `setTimeout`, `constructor`, `__proto__`, ...)
|
|
158
|
+
resolves to `undefined`. This blocks the common
|
|
159
|
+
`constructor.constructor("...")()` chain and accidental access to browser
|
|
160
|
+
globals from directive markup. See `docs/directives.md → Security model` for
|
|
161
|
+
the full contract.
|
|
162
|
+
- **`data-html` is now explicitly documented as XSS-prone.** Inline JSDoc
|
|
163
|
+
warning + Security model section in the docs. Sanitize untrusted input on
|
|
164
|
+
the server before binding.
|
|
165
|
+
|
|
166
|
+
### Fixed
|
|
167
|
+
|
|
168
|
+
- **`destroy()` actually unmounts.** Every DOM listener attached by
|
|
169
|
+
`data-on` / `@event` / `data-model` is now tracked on the instance and
|
|
170
|
+
removed in `destroy()`. Scheduled re-renders after destroy are no-ops.
|
|
171
|
+
Per-element bookkeeping flags are cleared so re-mounting the same DOM
|
|
172
|
+
rebinds cleanly.
|
|
173
|
+
- **Instance methods called from directive expressions now have `this`
|
|
174
|
+
bound to the component.** `data-text="doneCount() + ' done'"` where
|
|
175
|
+
`doneCount` reads `this.state.items` now works as written (previously
|
|
176
|
+
silently returned `undefined` due to `with()` semantics).
|
|
177
|
+
- **`data-model` on focused inputs syncs programmatic state changes.**
|
|
178
|
+
`this.state.q = ''` while the input has focus now clears the field.
|
|
179
|
+
Live typing remains a no-op (state already matches value after the input
|
|
180
|
+
event, so no write happens).
|
|
181
|
+
- **`data-model` on `<input type="number">` / `<input type="range">`
|
|
182
|
+
writes a `number`, not a string.** Empty inputs write `null`. Checkbox
|
|
183
|
+
inputs continue to write booleans.
|
|
184
|
+
- **Duplicate `data-each` keys produce a warning.** Previously rows
|
|
185
|
+
silently collided.
|
|
186
|
+
- **`null` / `undefined` `data-each` keys warn once per render**
|
|
187
|
+
instead of once per item.
|
|
188
|
+
- **`@event` shorthand re-scans the subtree on every render.**
|
|
189
|
+
Replaces the root-level `__micraAtScanned` flag with per-element
|
|
190
|
+
`__micraAtBound`, so `@click` attributes inside markup injected via
|
|
191
|
+
`data-html` get bound on the next render.
|
|
192
|
+
- **`data-bind="class:..."` + `data-class` on the same element now warns**
|
|
193
|
+
via `validateDirectives` — the two directives fight on every render.
|
|
194
|
+
- **`bus.off()` cleans up empty event Sets** instead of leaving them in
|
|
195
|
+
the map.
|
|
196
|
+
|
|
197
|
+
### Added
|
|
198
|
+
|
|
199
|
+
- **Dev warnings (deduped):**
|
|
200
|
+
- re-entrant `render()` call (typically: a directive expression that
|
|
201
|
+
mutated state) — warns once per instance.
|
|
202
|
+
- runtime errors in directive expressions — warns once per unique
|
|
203
|
+
expression string.
|
|
204
|
+
|
|
205
|
+
### Changed
|
|
206
|
+
|
|
207
|
+
- **Internal:** `data-bind` and `data-class` specs are pre-parsed once
|
|
208
|
+
into `CachedPairBinding.pairs` in `DirectiveCache` — re-renders skip
|
|
209
|
+
the comma+colon split.
|
|
210
|
+
- **Internal:** `Object.prototype` key membership is pre-computed once
|
|
211
|
+
at module load and cached in a `Set` (faster `safeStateHas`).
|
|
212
|
+
- **Internal:** `data-each` no-key warning is deduped per template
|
|
213
|
+
via `__micraNoKeyWarned`.
|
|
214
|
+
|
|
215
|
+
### Docs
|
|
216
|
+
|
|
217
|
+
- `docs/directives.md` — new "Security model" section.
|
|
218
|
+
- `docs/llm-guide.md` — Security model, `Micra.off` reference,
|
|
219
|
+
"Things Micra does NOT support" (key modifiers, nested `data-model`,
|
|
220
|
+
`data-if` keeps element in DOM).
|
|
221
|
+
- `docs/examples.md` — inline-edit example now has `data-ref="input"`
|
|
222
|
+
and uses `e.key === 'Enter'` instead of unsupported `@keydown.enter`.
|
|
223
|
+
|
|
224
|
+
### Bundle size
|
|
225
|
+
|
|
226
|
+
- ~3.7 KB → 4.8 KB gzip. Cost of the security hardening + listener
|
|
227
|
+
cleanup tracking.
|
|
228
|
+
|
|
229
|
+
### Migration notes
|
|
230
|
+
|
|
231
|
+
- If any directive expression relied on `constructor`, `window`, `fetch`,
|
|
232
|
+
or other non-whitelisted globals, it now resolves to `undefined`. Move
|
|
233
|
+
the access into a component method.
|
|
234
|
+
- If you held onto an instance after calling `destroy()`, you can now
|
|
235
|
+
safely re-mount the same DOM with a new definition.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## [1.0.0] — initial release
|
|
240
|
+
|
|
241
|
+
Reactive shallow `Proxy` state, batched microtask rendering, DOM directives
|
|
242
|
+
(`data-text`, `data-html`, `data-if`, `data-show`, `data-bind`, `data-model`,
|
|
243
|
+
`data-class`, `data-on`, `@event`), keyed `data-each` list rendering, event
|
|
244
|
+
bus, SSR `prop()`, `fetch()` helper, idempotent `start()`.
|
package/README.md
CHANGED
|
@@ -2,21 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
Micra.js is a lightweight reactive TypeScript framework for small sites and SaaS apps. It gives you reactive state, DOM directives, keyed list rendering, an event bus, SSR-friendly props, and auto-mounting in about 5 KB gzip.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## When to use Micra.js
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Built for **server-rendered apps** (Rails, Laravel, Django, Phoenix, ASP.NET) and small SaaS frontends that need a sprinkle of reactivity without a build step.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Reach for Micra.js instead of React/Vue when:
|
|
10
10
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
11
|
+
- ~5 KB gzip matters (full bundle, not "core")
|
|
12
|
+
- you want to drop a `<script>` tag on an existing page and go — no toolchain
|
|
13
|
+
- you have HTML rendered by your server template engine that needs reactive directives
|
|
14
|
+
- you don't need client-side routing or a full SPA
|
|
15
|
+
- you want **htmx + reactive client state** in the same page
|
|
16
|
+
|
|
17
|
+
Compared to Alpine.js: smaller surface, no `x-*` shorthand soup, AST-validated expressions (no global `window` / `fetch` access from markup), cleaner LLM ergonomics — fewer anti-patterns to fall into.
|
|
18
|
+
|
|
19
|
+
What you get:
|
|
20
|
+
|
|
21
|
+
- reactive `state` via a shallow `Proxy` (top-level writes only)
|
|
22
|
+
- JS expressions in directives: `data-if="count > 0"`
|
|
23
|
+
- keyed list diffing: `data-each` + `data-key`
|
|
24
|
+
- auto-mounting via `data-component` + `Micra.start()`
|
|
14
25
|
- SSR props from `data-*` attributes via `this.prop()`
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- DOM refs via `data-ref`
|
|
18
|
-
-
|
|
19
|
-
- simple lifecycle hooks: `onCreate`, `onDestroy`
|
|
26
|
+
- built-in `this.fetch()` helper with `AbortSignal` support
|
|
27
|
+
- global event bus: `Micra.on()` / `Micra.emit()`
|
|
28
|
+
- DOM refs via `data-ref`, additive class toggling via `data-class`
|
|
29
|
+
- lifecycle hooks: `onCreate`, `onDestroy`
|
|
20
30
|
|
|
21
31
|
## Quick Start
|
|
22
32
|
|
|
@@ -102,8 +112,8 @@ Micra.start();
|
|
|
102
112
|
| ------------ | ---------------------------------------- | ------------------------- |
|
|
103
113
|
| `data-text` | `data-text="name"` | Set `textContent` |
|
|
104
114
|
| `data-html` | `data-html="content"` | Set `innerHTML` |
|
|
105
|
-
| `data-if` | `data-if="count > 0"` |
|
|
106
|
-
| `data-show` | `data-show="loaded"` |
|
|
115
|
+
| `data-if` | `data-if="count > 0"` | Mount / unmount from DOM |
|
|
116
|
+
| `data-show` | `data-show="loaded"` | Toggle `style.display` |
|
|
107
117
|
| `data-bind` | `data-bind="href:url, disabled:loading"` | Bind attributes |
|
|
108
118
|
| `data-model` | `data-model="search"` | Two-way input binding |
|
|
109
119
|
| `data-each` | `data-each="items" data-key="id"` | List rendering |
|
|
@@ -153,13 +163,14 @@ this.refs
|
|
|
153
163
|
this.render()
|
|
154
164
|
this.destroy()
|
|
155
165
|
this.prop(name, default?)
|
|
156
|
-
this.fetch(url, options?)
|
|
166
|
+
this.fetch(url, options?) // supports AbortSignal in options.signal
|
|
157
167
|
this.emit(event, payload?)
|
|
158
168
|
this.on(event, handler)
|
|
159
169
|
```
|
|
160
170
|
|
|
161
171
|
## Documentation
|
|
162
172
|
|
|
173
|
+
- **AI / LLM code generation:** [`llms.txt`](./llms.txt) (overview) · [`llms-full.txt`](./llms-full.txt) (10 inline recipes + anti-pattern reference) · [`docs/llm-guide.md`](./docs/llm-guide.md) (full guide)
|
|
163
174
|
- [Getting started](./docs/getting-started.md)
|
|
164
175
|
- [Core concepts](./docs/concepts.md)
|
|
165
176
|
- [Directives](./docs/directives.md)
|
|
@@ -171,3 +182,17 @@ this.on(event, handler)
|
|
|
171
182
|
- Recipes:
|
|
172
183
|
- [Todo app](./docs/recipes/todo-app.md)
|
|
173
184
|
- [Server-sent events (SSE)](./docs/recipes/sse.md)
|
|
185
|
+
|
|
186
|
+
## Code generation with LLMs
|
|
187
|
+
|
|
188
|
+
Micra has a small surface area, but LLMs default to jQuery / vanilla-JS or React patterns that defeat the framework. When generating Micra code (in Claude artifacts, ChatGPT canvas, Cursor, Copilot, etc.), follow these rules:
|
|
189
|
+
|
|
190
|
+
1. **Lists** go through `<template data-each="items" data-key="id">`. Never `getElementById` / `innerHTML` for component output.
|
|
191
|
+
2. **Derived values** (counts, totals, filtered subsets) are **methods**, not state fields. State holds raw data only.
|
|
192
|
+
3. **Event handlers** use `@event` / `data-on`. Never `addEventListener` inside methods — it leaks past `destroy()`. Document-level listeners go in `onCreate` and are removed in `onDestroy`.
|
|
193
|
+
4. **No manual re-render.** Micra batches a microtask render on every state write — no `this.refresh()` / `this.update()` / `this.renderList()`.
|
|
194
|
+
5. **State proxy is shallow.** Replace top-level: `state.user = { ...state.user, name: x }`, not `state.user.name = x`.
|
|
195
|
+
6. **No modifier syntax** like `@keydown.enter` — branch on `e.key === 'Enter'` inside the handler.
|
|
196
|
+
7. **Use jsDelivr, not unpkg** — `cdn.jsdelivr.net` is in the CSP allowlist of Claude artifacts / ChatGPT canvas; `unpkg.com` is blocked there.
|
|
197
|
+
|
|
198
|
+
Full anti-pattern reference with side-by-side examples: [`docs/llm-guide.md`](./docs/llm-guide.md) and [`llms-full.txt`](./llms-full.txt).
|
package/dist/core/mount.d.ts
CHANGED
|
@@ -11,17 +11,23 @@
|
|
|
11
11
|
* mount() is called by both the public Micra.mount() API and by start()
|
|
12
12
|
* (which scans the DOM for [data-component] elements).
|
|
13
13
|
*/
|
|
14
|
-
import type { ComponentDefinition, ComponentInstance, StateRecord } from
|
|
14
|
+
import type { ComponentDefinition, ComponentInstance, StateRecord } from "../types";
|
|
15
15
|
/**
|
|
16
16
|
* Mount a component definition onto a DOM element.
|
|
17
17
|
* Returns the component instance, or null if the root element is not found.
|
|
18
18
|
*
|
|
19
19
|
* Already-mounted elements return the existing instance.
|
|
20
20
|
*
|
|
21
|
+
* Both `S` (state) and `M` (methods) are inferred from the literal — the
|
|
22
|
+
* returned instance is fully typed: `inst.state.X` and `inst.someMethod()`
|
|
23
|
+
* are checked.
|
|
24
|
+
*
|
|
21
25
|
* @example
|
|
22
26
|
* const instance = Micra.mount('#counter', {
|
|
23
27
|
* state: { count: 0 },
|
|
24
28
|
* inc() { this.state.count++ },
|
|
25
29
|
* })
|
|
30
|
+
* instance?.inc()
|
|
31
|
+
* instance?.state.count
|
|
26
32
|
*/
|
|
27
|
-
export declare function mount<S extends StateRecord>(selector: string | HTMLElement, definition: ComponentDefinition<S>): ComponentInstance<S> | null;
|
|
33
|
+
export declare function mount<S extends StateRecord, M>(selector: string | HTMLElement, definition: ComponentDefinition<S, M>): ComponentInstance<S, M> | null;
|
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/core/registry.d.ts
CHANGED
|
@@ -14,13 +14,22 @@ export declare const _instances: Map<HTMLElement, InternalInstance<StateRecord>>
|
|
|
14
14
|
/**
|
|
15
15
|
* Register a component definition under `name`.
|
|
16
16
|
*
|
|
17
|
+
* Both state shape (`S`) and method set (`M`) are inferred from the literal,
|
|
18
|
+
* so `this.state.X` and `this.someMethod()` are fully typed inside the
|
|
19
|
+
* method bodies and lifecycle hooks.
|
|
20
|
+
*
|
|
17
21
|
* @example
|
|
18
|
-
* define('counter', {
|
|
22
|
+
* define('counter', {
|
|
23
|
+
* state: { count: 0 },
|
|
24
|
+
* inc() { this.state.count++ }, // this.state.count: number ✓
|
|
25
|
+
* reset() { this.state.count = 0 }, // this.reset() is also typed ✓
|
|
26
|
+
* onCreate() { this.inc() }, // ✓
|
|
27
|
+
* })
|
|
19
28
|
*/
|
|
20
|
-
export declare function define<S extends StateRecord>(name: string, definition: ComponentDefinition<S>): void;
|
|
29
|
+
export declare function define<S extends StateRecord, M>(name: string, definition: ComponentDefinition<S, M>): void;
|
|
21
30
|
/**
|
|
22
31
|
* Type-helper — returns `definition` unchanged but lets TypeScript infer `S`
|
|
23
|
-
* from the
|
|
32
|
+
* and `M` from the literal so all methods are typed with the correct `this`.
|
|
24
33
|
*
|
|
25
34
|
* Use this when defining a component outside a `define()` call.
|
|
26
35
|
*
|
|
@@ -31,7 +40,7 @@ export declare function define<S extends StateRecord>(name: string, definition:
|
|
|
31
40
|
* })
|
|
32
41
|
* Micra.define('counter', counter)
|
|
33
42
|
*/
|
|
34
|
-
export declare function defineComponent<S extends StateRecord>(definition: ComponentDefinition<S>): ComponentDefinition<S>;
|
|
43
|
+
export declare function defineComponent<S extends StateRecord, M>(definition: ComponentDefinition<S, M>): ComponentDefinition<S, M>;
|
|
35
44
|
/**
|
|
36
45
|
* Returns a read-only view of all live instances (keyed by root element).
|
|
37
46
|
* Useful for DevTools / debugging.
|
package/dist/dom/directives.d.ts
CHANGED
|
@@ -4,36 +4,32 @@
|
|
|
4
4
|
* Responsibilities:
|
|
5
5
|
* - data-text, data-html, data-if, data-show, data-bind, data-model
|
|
6
6
|
* - data-class (additive class toggling)
|
|
7
|
-
* - Directive result cache (built once per element, reused on re-renders)
|
|
8
7
|
*
|
|
9
|
-
* LLM NOTE: applyDirectives() is called on every render.
|
|
10
|
-
* (
|
|
11
|
-
*
|
|
8
|
+
* LLM NOTE: applyDirectives() is called on every render. It consumes a
|
|
9
|
+
* pre-computed ScanIndex (built once by scan.ts and cached on the element).
|
|
10
|
+
* The scan replaced 10+ querySelectorAll calls with a single TreeWalker pass.
|
|
12
11
|
*
|
|
13
12
|
* Important: this module does NOT handle data-each — see dom/each.ts.
|
|
14
13
|
*/
|
|
15
|
-
import type { InternalInstance, StateRecord } from '../types';
|
|
14
|
+
import type { InternalInstance, ScanIndex, StateRecord } from '../types';
|
|
16
15
|
import { warn } from '../utils/expr';
|
|
17
16
|
/**
|
|
18
17
|
* Apply all non-each directives to a component subtree.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Consumes a pre-computed ScanIndex. data-if runs first so subsequent
|
|
20
|
+
* directives don't write into a tree that's about to be detached this tick.
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
* fragments are new clones on every render.
|
|
25
|
-
*
|
|
26
|
-
* @param root - Component root Element or DocumentFragment (no-key each clone)
|
|
22
|
+
* @param scan - Pre-computed scan from scan.ts (cached per element)
|
|
27
23
|
* @param state - Expression state (may include item/index for each rows)
|
|
28
24
|
* @param rawState - Raw (non-proxy) state for model sync
|
|
29
|
-
* @param instance - Component instance (unused here, kept for future hooks)
|
|
30
25
|
*/
|
|
31
|
-
export declare function applyDirectives<S extends StateRecord>(
|
|
26
|
+
export declare function applyDirectives<S extends StateRecord>(scan: ScanIndex, state: StateRecord, rawState: StateRecord, _instance: InternalInstance<S>): void;
|
|
32
27
|
/**
|
|
33
28
|
* Validate directive usage and emit dev warnings.
|
|
34
|
-
* Called once after the initial render of a component
|
|
29
|
+
* Called once after the initial render of a component, with the already-built
|
|
30
|
+
* scan so we don't walk the DOM again.
|
|
35
31
|
*
|
|
36
32
|
* @internal
|
|
37
33
|
*/
|
|
38
|
-
export declare function validateDirectives(
|
|
34
|
+
export declare function validateDirectives(scan: ScanIndex): void;
|
|
39
35
|
export { warn };
|
package/dist/dom/each.d.ts
CHANGED
|
@@ -8,18 +8,21 @@
|
|
|
8
8
|
* - Apply directives to each row with a scoped itemState
|
|
9
9
|
*
|
|
10
10
|
* LLM NOTE: renderList() is called on every render cycle AFTER applyDirectives().
|
|
11
|
-
*
|
|
11
|
+
* The template list comes pre-scanned from scan.ts — no DOM queries here.
|
|
12
|
+
* Each row node gets its own ScanIndex cached on `node.__micraScan` so
|
|
13
|
+
* re-renders of that row don't re-walk the DOM.
|
|
12
14
|
* Keyed mode (data-key present) mutates the DOM in-place — nodes are
|
|
13
15
|
* created once and reused. Non-keyed mode removes all nodes and re-clones.
|
|
14
16
|
*/
|
|
15
17
|
import type { InternalInstance, StateRecord } from '../types';
|
|
16
18
|
/**
|
|
17
|
-
* Process all `<template data-each>` elements
|
|
19
|
+
* Process all `<template data-each>` elements found by the scanner.
|
|
18
20
|
* Scoped itemState makes `item`, `index`, `$index` available in row expressions.
|
|
19
21
|
*
|
|
20
|
-
* @param
|
|
21
|
-
* @param state
|
|
22
|
-
* @param rawState
|
|
23
|
-
* @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)
|
|
24
27
|
*/
|
|
25
|
-
export declare function renderList<S extends StateRecord>(
|
|
28
|
+
export declare function renderList<S extends StateRecord>(templates: Element[], state: StateRecord, rawState: StateRecord, instance: InternalInstance<S>, triggerKey: string | null | 'MULTIPLE'): void;
|
package/dist/dom/events.d.ts
CHANGED
|
@@ -9,29 +9,36 @@
|
|
|
9
9
|
* LLM NOTE: Every listener attached here is also recorded in
|
|
10
10
|
* instance.__micraListeners so destroy() can remove it cleanly.
|
|
11
11
|
* Re-render skips already-bound elements via per-element __micra* flags.
|
|
12
|
+
*
|
|
13
|
+
* All three binders accept pre-computed element lists from scan.ts —
|
|
14
|
+
* no DOM queries here.
|
|
12
15
|
*/
|
|
13
|
-
import type { InternalInstance, StateRecord } from '../types';
|
|
16
|
+
import type { CachedBinding, InternalInstance, StateRecord } from '../types';
|
|
14
17
|
/**
|
|
15
18
|
* Bind `data-on="event:method[,event2:method2]"` listeners.
|
|
16
19
|
* Listeners are bound once — re-render calls are no-ops for already-bound elements.
|
|
17
20
|
*
|
|
18
21
|
* Supports modifiers: `click.prevent`, `click.stop`, `click.self`.
|
|
19
22
|
*
|
|
23
|
+
* @param els - Pre-computed list of [data-on] elements from scan.ts
|
|
24
|
+
*
|
|
20
25
|
* @example
|
|
21
26
|
* <button data-on="click:save">Save</button>
|
|
22
27
|
* <form data-on="submit.prevent:handleSubmit">
|
|
23
28
|
*/
|
|
24
|
-
export declare function bindDataOn<S extends StateRecord>(
|
|
29
|
+
export declare function bindDataOn<S extends StateRecord>(els: Element[], instance: InternalInstance<S>): void;
|
|
25
30
|
/**
|
|
26
31
|
* Bind `@event="method"` shorthand attributes (Stimulus-style).
|
|
27
32
|
* Bound once per element via `__micraAtBound` — re-renders are no-ops.
|
|
28
|
-
*
|
|
33
|
+
*
|
|
34
|
+
* @param els - Pre-computed list of elements with at least one @-prefixed attr
|
|
35
|
+
* (from scan.ts — replaces the old `querySelectorAll('*')` walk)
|
|
29
36
|
*
|
|
30
37
|
* @example
|
|
31
38
|
* <button @click="increment">+</button>
|
|
32
39
|
* <form @submit.prevent="handleSubmit">
|
|
33
40
|
*/
|
|
34
|
-
export declare function bindAtEvents<S extends StateRecord>(
|
|
41
|
+
export declare function bindAtEvents<S extends StateRecord>(els: Element[], instance: InternalInstance<S>): void;
|
|
35
42
|
/**
|
|
36
43
|
* Two-way binding: `data-model="key"` wires <input>/<select>/<textarea>
|
|
37
44
|
* to `state[key]`. Binding is attached once per element.
|
|
@@ -39,8 +46,11 @@ export declare function bindAtEvents<S extends StateRecord>(root: Element, insta
|
|
|
39
46
|
* Numeric inputs (`type="number"` / `type="range"`) write numbers, not strings.
|
|
40
47
|
* Checkbox inputs write booleans. Everything else writes strings.
|
|
41
48
|
*
|
|
49
|
+
* @param bindings - Pre-computed model bindings from scan.ts
|
|
50
|
+
* (each carries { el, expr } where expr is the state key)
|
|
51
|
+
*
|
|
42
52
|
* @example
|
|
43
53
|
* <input data-model="search"> // updates state.search on every keystroke
|
|
44
54
|
* <select data-model="sortBy"> // updates state.sortBy on change
|
|
45
55
|
*/
|
|
46
|
-
export declare function bindModels<S extends StateRecord>(
|
|
56
|
+
export declare function bindModels<S extends StateRecord>(bindings: CachedBinding[], instance: InternalInstance<S>): void;
|
package/dist/dom/refs.d.ts
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
* src/dom/refs.ts — data-ref collection.
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities:
|
|
5
|
-
* -
|
|
6
|
-
* - Populate `instance.refs` so methods can do `this.refs.chart` etc.
|
|
5
|
+
* - Populate `instance.refs` from a pre-scanned list of [data-ref] elements.
|
|
7
6
|
*
|
|
8
7
|
* LLM NOTE: This module is PURE relative to state — it only reads DOM attributes
|
|
9
8
|
* and writes to instance.refs. It does NOT trigger renders.
|
|
10
9
|
*/
|
|
11
10
|
import type { InternalInstance, StateRecord } from '../types';
|
|
12
11
|
/**
|
|
13
|
-
*
|
|
14
|
-
* `instance.refs`.
|
|
12
|
+
* Build `instance.refs` from the pre-scanned [data-ref] elements.
|
|
15
13
|
*
|
|
16
14
|
* Called once after the initial render and again on every re-render (refs may
|
|
17
15
|
* point to newly created elements after an each-list update).
|
|
18
16
|
*
|
|
17
|
+
* @param els - List of [data-ref] elements from scan.ts
|
|
18
|
+
*
|
|
19
19
|
* @example
|
|
20
20
|
* // HTML: <canvas data-ref="chart">
|
|
21
21
|
* // JS: this.refs.chart → HTMLCanvasElement
|
|
22
22
|
*/
|
|
23
|
-
export declare function collectRefs<S extends StateRecord>(
|
|
23
|
+
export declare function collectRefs<S extends StateRecord>(els: Element[], instance: InternalInstance<S>): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/dom/scan.ts — Single-pass directive/event/ref scanner.
|
|
3
|
+
*
|
|
4
|
+
* Replaces 10+ querySelectorAll calls per render with ONE TreeWalker
|
|
5
|
+
* traversal that classifies every directive attribute in a single visit.
|
|
6
|
+
*
|
|
7
|
+
* Boundaries:
|
|
8
|
+
* - REJECT (skip subtree) on nested [data-component] — same semantics as
|
|
9
|
+
* the old `filterOwn` helper, but applied during the walk so we don't
|
|
10
|
+
* even *visit* those nodes.
|
|
11
|
+
* - <template> contents are not visited (browser TreeWalker default).
|
|
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.
|
|
14
|
+
*
|
|
15
|
+
* Hot-path notes:
|
|
16
|
+
* - We read `el.attributes` once and switch by suffix. No allocations per
|
|
17
|
+
* non-matching attr.
|
|
18
|
+
* - Pair-parsing (`data-bind`, `data-class`) happens here, once, at scan
|
|
19
|
+
* time. Reused on every render.
|
|
20
|
+
*/
|
|
21
|
+
import type { ScanIndex } from "../types";
|
|
22
|
+
/**
|
|
23
|
+
* Scan an Element subtree owned by one component. Skips nested
|
|
24
|
+
* [data-component] subtrees entirely. Visits the root itself.
|
|
25
|
+
*
|
|
26
|
+
* Cached on `el.__micraScan` after the first call — subsequent renders
|
|
27
|
+
* are free.
|
|
28
|
+
*/
|
|
29
|
+
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, ComponentInstance, ComponentDefinition, } from './types';
|
|
24
|
+
export type { StateRecord, UnsubFn, EventHandler, 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';
|