micra.js 2.1.0 → 2.2.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 +231 -0
- package/README.md +39 -14
- package/dist/core/mount.d.ts +8 -2
- package/dist/core/registry.d.ts +13 -4
- package/dist/dom/directives.d.ts +11 -15
- package/dist/dom/each.d.ts +6 -4
- 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 +196 -143
- package/dist/micra.cjs.js.map +4 -4
- package/dist/micra.esm.js +196 -143
- package/dist/micra.esm.js.map +4 -4
- package/dist/micra.js +196 -143
- package/dist/micra.js.map +4 -4
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +63 -21
- package/llms-full.txt +600 -0
- package/llms.txt +148 -0
- package/package.json +11 -3
- package/src/core/mount.ts +126 -98
- package/src/core/registry.ts +19 -9
- package/src/dom/directives.ts +34 -120
- package/src/dom/each.ts +36 -19
- package/src/dom/events.ts +23 -31
- package/src/dom/refs.ts +6 -7
- package/src/dom/scan.ts +189 -0
- package/src/index.ts +2 -0
- package/src/types.ts +76 -21
package/src/dom/directives.ts
CHANGED
|
@@ -4,27 +4,21 @@
|
|
|
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
14
|
|
|
16
15
|
import type {
|
|
17
|
-
CachedBinding,
|
|
18
16
|
CachedIfBinding,
|
|
19
|
-
CachedPairBinding,
|
|
20
|
-
DirectiveCache,
|
|
21
17
|
InternalInstance,
|
|
22
|
-
|
|
23
|
-
MicraTemplate,
|
|
18
|
+
ScanIndex,
|
|
24
19
|
StateRecord,
|
|
25
20
|
} from '../types'
|
|
26
21
|
import { evalExpr, warn } from '../utils/expr'
|
|
27
|
-
import { queryOwn, queryAll } from './query'
|
|
28
22
|
|
|
29
23
|
// ── Directive appliers ────────────────────────────────────────────────────────
|
|
30
24
|
// Each function is PURE relative to state — reads state, writes DOM.
|
|
@@ -110,13 +104,8 @@ function applyBind(
|
|
|
110
104
|
|
|
111
105
|
/**
|
|
112
106
|
* data-class="active:isActive, disabled:count === 0"
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* Syntax mirrors data-bind — split by comma, then by first colon.
|
|
117
|
-
*
|
|
118
|
-
* @example
|
|
119
|
-
* <div data-class="active:tab === 'home', hidden:!loaded">
|
|
107
|
+
* Toggles classes additively (does NOT replace full className like data-bind:class).
|
|
108
|
+
* Pairs are pre-parsed at scan time.
|
|
120
109
|
*/
|
|
121
110
|
function applyClass(
|
|
122
111
|
el: Element,
|
|
@@ -128,20 +117,6 @@ function applyClass(
|
|
|
128
117
|
}
|
|
129
118
|
}
|
|
130
119
|
|
|
131
|
-
/** @internal Parse a comma+colon spec like `href:url, disabled:loading` once. */
|
|
132
|
-
function parsePairs(expr: string): Array<readonly [string, string]> {
|
|
133
|
-
const out: Array<readonly [string, string]> = []
|
|
134
|
-
for (const part of expr.split(',')) {
|
|
135
|
-
const colonIdx = part.indexOf(':')
|
|
136
|
-
if (colonIdx === -1) continue
|
|
137
|
-
const left = part.slice(0, colonIdx).trim()
|
|
138
|
-
const right = part.slice(colonIdx + 1).trim()
|
|
139
|
-
if (!left) continue
|
|
140
|
-
out.push([left, right])
|
|
141
|
-
}
|
|
142
|
-
return out
|
|
143
|
-
}
|
|
144
|
-
|
|
145
120
|
function applyModel(
|
|
146
121
|
el: Element,
|
|
147
122
|
key: string,
|
|
@@ -157,126 +132,65 @@ function applyModel(
|
|
|
157
132
|
// listener is attached separately in events.ts — this only syncs the value
|
|
158
133
|
}
|
|
159
134
|
|
|
160
|
-
// ── Directive cache ───────────────────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
/** @internal Collect all directive bindings for a root element. Built once. */
|
|
163
|
-
function buildCache(root: Element): DirectiveCache {
|
|
164
|
-
const pick = (attr: string): CachedBinding[] => {
|
|
165
|
-
const els = queryOwn(root, attr)
|
|
166
|
-
// Include root itself
|
|
167
|
-
if ((root as HTMLElement).hasAttribute?.(attr)) els.unshift(root)
|
|
168
|
-
return els
|
|
169
|
-
.filter(el => !el.closest('template'))
|
|
170
|
-
.map(el => ({ el, expr: el.getAttribute(attr)! }))
|
|
171
|
-
}
|
|
172
|
-
const pickPairs = (attr: string): CachedPairBinding[] =>
|
|
173
|
-
pick(attr).map(b => ({ ...b, pairs: parsePairs(b.expr) }))
|
|
174
|
-
return {
|
|
175
|
-
text: pick('data-text'),
|
|
176
|
-
html: pick('data-html'),
|
|
177
|
-
if: pick('data-if') as CachedIfBinding[],
|
|
178
|
-
show: pick('data-show'),
|
|
179
|
-
bind: pickPairs('data-bind'),
|
|
180
|
-
model: pick('data-model'),
|
|
181
|
-
class: pickPairs('data-class'),
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
135
|
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
186
136
|
|
|
187
137
|
/**
|
|
188
138
|
* Apply all non-each directives to a component subtree.
|
|
189
139
|
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* For DocumentFragments (no-key each clones): always re-scan because these
|
|
194
|
-
* fragments are new clones on every render.
|
|
140
|
+
* Consumes a pre-computed ScanIndex. data-if runs first so subsequent
|
|
141
|
+
* directives don't write into a tree that's about to be detached this tick.
|
|
195
142
|
*
|
|
196
|
-
* @param
|
|
143
|
+
* @param scan - Pre-computed scan from scan.ts (cached per element)
|
|
197
144
|
* @param state - Expression state (may include item/index for each rows)
|
|
198
145
|
* @param rawState - Raw (non-proxy) state for model sync
|
|
199
|
-
* @param instance - Component instance (unused here, kept for future hooks)
|
|
200
146
|
*/
|
|
201
147
|
export function applyDirectives<S extends StateRecord>(
|
|
202
|
-
|
|
148
|
+
scan: ScanIndex,
|
|
203
149
|
state: StateRecord,
|
|
204
150
|
rawState: StateRecord,
|
|
205
151
|
_instance: InternalInstance<S>,
|
|
206
|
-
): void {
|
|
207
|
-
// DocumentFragments are temporary clones — always scan, never cache
|
|
208
|
-
if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
|
209
|
-
applyFromList(buildFragmentList(root as DocumentFragment), state, rawState)
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const el = root as MicraElement
|
|
214
|
-
if (!el.__micraCache) el.__micraCache = buildCache(el)
|
|
215
|
-
applyFromList(el.__micraCache, state, rawState)
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** @internal Apply a pre-built cache / binding list to current state. */
|
|
219
|
-
function applyFromList(
|
|
220
|
-
cache: DirectiveCache,
|
|
221
|
-
state: StateRecord,
|
|
222
|
-
rawState: StateRecord,
|
|
223
152
|
): void {
|
|
224
153
|
// data-if runs first so subsequent directives don't write into a tree that's
|
|
225
154
|
// about to be detached this tick.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/** @internal Scan a DocumentFragment (no-key each clone) — returns a DirectiveCache. */
|
|
236
|
-
function buildFragmentList(frag: DocumentFragment): DirectiveCache {
|
|
237
|
-
const pick = (attr: string): CachedBinding[] =>
|
|
238
|
-
queryAll(frag, `[${attr}]`)
|
|
239
|
-
.filter(el => !el.closest('template'))
|
|
240
|
-
.map(el => ({ el, expr: el.getAttribute(attr)! }))
|
|
241
|
-
const pickPairs = (attr: string): CachedPairBinding[] =>
|
|
242
|
-
pick(attr).map(b => ({ ...b, pairs: parsePairs(b.expr) }))
|
|
243
|
-
return {
|
|
244
|
-
text: pick('data-text'),
|
|
245
|
-
html: pick('data-html'),
|
|
246
|
-
if: pick('data-if') as CachedIfBinding[],
|
|
247
|
-
show: pick('data-show'),
|
|
248
|
-
bind: pickPairs('data-bind'),
|
|
249
|
-
model: pick('data-model'),
|
|
250
|
-
class: pickPairs('data-class'),
|
|
251
|
-
}
|
|
155
|
+
for (const b of scan.if) applyIf(b, state)
|
|
156
|
+
for (const b of scan.text) applyText(b.el, b.expr, state)
|
|
157
|
+
for (const b of scan.html) applyHtml(b.el, b.expr, state)
|
|
158
|
+
for (const b of scan.show) applyShow(b.el, b.expr, state)
|
|
159
|
+
for (const b of scan.bind) applyBind(b.el, b.pairs, state)
|
|
160
|
+
for (const b of scan.model) applyModel(b.el, b.expr.trim(), rawState)
|
|
161
|
+
for (const b of scan.class) applyClass(b.el, b.pairs, state)
|
|
252
162
|
}
|
|
253
163
|
|
|
254
164
|
// ── Dev warning helper ────────────────────────────────────────────────────────
|
|
255
165
|
|
|
256
166
|
/**
|
|
257
167
|
* Validate directive usage and emit dev warnings.
|
|
258
|
-
* Called once after the initial render of a component
|
|
168
|
+
* Called once after the initial render of a component, with the already-built
|
|
169
|
+
* scan so we don't walk the DOM again.
|
|
259
170
|
*
|
|
260
171
|
* @internal
|
|
261
172
|
*/
|
|
262
|
-
export function validateDirectives(
|
|
263
|
-
|
|
264
|
-
const tmpl = el as
|
|
173
|
+
export function validateDirectives(scan: ScanIndex): void {
|
|
174
|
+
for (const el of scan.each) {
|
|
175
|
+
const tmpl = el as HTMLTemplateElement & { __micraNoKeyWarned?: true }
|
|
265
176
|
if (!el.hasAttribute('data-key') && !tmpl.__micraNoKeyWarned) {
|
|
266
177
|
tmpl.__micraNoKeyWarned = true
|
|
267
|
-
warn(
|
|
178
|
+
warn(
|
|
179
|
+
`data-each="${el.getAttribute('data-each')}" has no data-key — ` +
|
|
180
|
+
`keyed diff disabled. Add data-key="id" for better performance.`,
|
|
181
|
+
)
|
|
268
182
|
}
|
|
269
|
-
}
|
|
183
|
+
}
|
|
270
184
|
|
|
271
185
|
// data-bind="class:..." replaces className wholesale, which fights with
|
|
272
186
|
// data-class on the same element. Warn so the developer picks one.
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
187
|
+
for (const b of scan.bind) {
|
|
188
|
+
const hasClassBind = b.pairs.some(p => p[0] === 'class')
|
|
189
|
+
if (hasClassBind && b.el.hasAttribute('data-class')) {
|
|
190
|
+
warn(
|
|
191
|
+
`element has both data-bind="class:..." and data-class — they fight ` +
|
|
192
|
+
`on every render. Use one.`,
|
|
193
|
+
)
|
|
280
194
|
}
|
|
281
195
|
}
|
|
282
196
|
}
|
package/src/dom/each.ts
CHANGED
|
@@ -8,34 +8,41 @@
|
|
|
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
|
|
|
16
|
-
import type {
|
|
18
|
+
import type {
|
|
19
|
+
InternalInstance,
|
|
20
|
+
MicraElement,
|
|
21
|
+
MicraTemplate,
|
|
22
|
+
StateRecord,
|
|
23
|
+
} from '../types'
|
|
17
24
|
import { evalExpr, warn } from '../utils/expr'
|
|
18
25
|
import { applyDirectives } from './directives'
|
|
19
|
-
import { bindDataOn, bindAtEvents } from './events'
|
|
20
|
-
import {
|
|
26
|
+
import { bindDataOn, bindAtEvents, bindModels } from './events'
|
|
27
|
+
import { scanComponent, scanFragment } from './scan'
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
|
-
* Process all `<template data-each>` elements
|
|
30
|
+
* Process all `<template data-each>` elements found by the scanner.
|
|
24
31
|
* Scoped itemState makes `item`, `index`, `$index` available in row expressions.
|
|
25
32
|
*
|
|
26
|
-
* @param
|
|
33
|
+
* @param templates - Pre-scanned list of <template data-each> elements
|
|
27
34
|
* @param state - Expression state (proxy merging rawState + instance)
|
|
28
35
|
* @param rawState - Raw (non-proxy) state — used for model binding
|
|
29
36
|
* @param instance - Component instance (for event binding)
|
|
30
37
|
*/
|
|
31
38
|
export function renderList<S extends StateRecord>(
|
|
32
|
-
|
|
39
|
+
templates: Element[],
|
|
33
40
|
state: StateRecord,
|
|
34
41
|
rawState: StateRecord,
|
|
35
42
|
instance: InternalInstance<S>,
|
|
36
43
|
): void {
|
|
37
|
-
|
|
38
|
-
if (tmplEl.tagName !== 'TEMPLATE')
|
|
44
|
+
for (const tmplEl of templates) {
|
|
45
|
+
if (tmplEl.tagName !== 'TEMPLATE') continue
|
|
39
46
|
const tmpl = tmplEl as MicraTemplate
|
|
40
47
|
|
|
41
48
|
const itemsExpr = tmpl.getAttribute('data-each')!
|
|
@@ -56,14 +63,14 @@ export function renderList<S extends StateRecord>(
|
|
|
56
63
|
const parent = marker.parentNode
|
|
57
64
|
// The template (and its marker) is currently detached — likely a data-if
|
|
58
65
|
// ancestor unmounted this subtree. Nothing to do until it returns.
|
|
59
|
-
if (!parent)
|
|
66
|
+
if (!parent) continue
|
|
60
67
|
|
|
61
68
|
// Empty / non-array: clear all rendered rows
|
|
62
69
|
if (!Array.isArray(items)) {
|
|
63
70
|
tmpl.__micraList.forEach(n => n.remove())
|
|
64
71
|
tmpl.__micraList = []
|
|
65
72
|
keyMap.clear()
|
|
66
|
-
|
|
73
|
+
continue
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
if (keyAttr) {
|
|
@@ -71,7 +78,7 @@ export function renderList<S extends StateRecord>(
|
|
|
71
78
|
} else {
|
|
72
79
|
renderNoKey(tmpl, items as StateRecord[], marker, parent, state, rawState, instance)
|
|
73
80
|
}
|
|
74
|
-
}
|
|
81
|
+
}
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
// ── Keyed diff ────────────────────────────────────────────────────────────────
|
|
@@ -118,16 +125,23 @@ function renderKeyed<S extends StateRecord>(
|
|
|
118
125
|
}
|
|
119
126
|
node.__micraKey = key
|
|
120
127
|
keyMap.set(key, node)
|
|
121
|
-
// Bind data-on
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
// Bind data-on / @event / data-model listeners once per row node.
|
|
129
|
+
// Scan the row, cache the scan on the node for future re-renders.
|
|
130
|
+
const rowScan = scanComponent(node)
|
|
131
|
+
node.__micraScan = rowScan
|
|
132
|
+
bindDataOn(rowScan.on, instance)
|
|
133
|
+
bindAtEvents(rowScan.atEvents, instance)
|
|
134
|
+
bindModels(rowScan.model, instance)
|
|
124
135
|
}
|
|
125
136
|
|
|
126
137
|
const itemState = Object.assign(
|
|
127
138
|
Object.create(state) as StateRecord,
|
|
128
139
|
{ item, index, $index: index },
|
|
129
140
|
)
|
|
130
|
-
|
|
141
|
+
// Use the cached scan if present (created above on first sight of this key);
|
|
142
|
+
// older paths may pass a node we haven't scanned yet.
|
|
143
|
+
const rowScan = node.__micraScan ?? (node.__micraScan = scanComponent(node))
|
|
144
|
+
applyDirectives(rowScan, itemState, rawState, instance)
|
|
131
145
|
nextNodes.push(node)
|
|
132
146
|
}
|
|
133
147
|
|
|
@@ -167,9 +181,12 @@ function renderNoKey<S extends StateRecord>(
|
|
|
167
181
|
Object.create(state) as StateRecord,
|
|
168
182
|
{ item, index, $index: index },
|
|
169
183
|
)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
184
|
+
// Fresh clone each render → fresh scan each render (uncached).
|
|
185
|
+
const fragScan = scanFragment(clone)
|
|
186
|
+
applyDirectives(fragScan, itemState, rawState, instance)
|
|
187
|
+
bindDataOn(fragScan.on, instance)
|
|
188
|
+
bindAtEvents(fragScan.atEvents, instance)
|
|
189
|
+
bindModels(fragScan.model, instance)
|
|
173
190
|
|
|
174
191
|
const nodes = Array.from(clone.childNodes) as MicraElement[]
|
|
175
192
|
nodes.forEach(n => { n.__micraEach = true; frag.append(n) })
|
package/src/dom/events.ts
CHANGED
|
@@ -9,11 +9,18 @@
|
|
|
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
16
|
|
|
14
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
CachedBinding,
|
|
19
|
+
InternalInstance,
|
|
20
|
+
MicraElement,
|
|
21
|
+
StateRecord,
|
|
22
|
+
} from '../types'
|
|
15
23
|
import { warn } from '../utils/expr'
|
|
16
|
-
import { queryOwn, queryOwnAll, queryAll } from './query'
|
|
17
24
|
|
|
18
25
|
/** @internal Attach a DOM listener and track it on the instance for destroy(). */
|
|
19
26
|
function track<S extends StateRecord>(
|
|
@@ -34,23 +41,16 @@ function track<S extends StateRecord>(
|
|
|
34
41
|
*
|
|
35
42
|
* Supports modifiers: `click.prevent`, `click.stop`, `click.self`.
|
|
36
43
|
*
|
|
44
|
+
* @param els - Pre-computed list of [data-on] elements from scan.ts
|
|
45
|
+
*
|
|
37
46
|
* @example
|
|
38
47
|
* <button data-on="click:save">Save</button>
|
|
39
48
|
* <form data-on="submit.prevent:handleSubmit">
|
|
40
49
|
*/
|
|
41
50
|
export function bindDataOn<S extends StateRecord>(
|
|
42
|
-
|
|
51
|
+
els: Element[],
|
|
43
52
|
instance: InternalInstance<S>,
|
|
44
53
|
): void {
|
|
45
|
-
const isFragment = root.nodeType === 11
|
|
46
|
-
const els = isFragment
|
|
47
|
-
? queryAll(root as unknown as ParentNode, '[data-on]')
|
|
48
|
-
: queryOwn(root, 'data-on')
|
|
49
|
-
|
|
50
|
-
// Include root itself if it carries data-on (e.g., the keyed item IS the button)
|
|
51
|
-
if (!isFragment && (root as HTMLElement).hasAttribute?.('data-on') && !els.includes(root))
|
|
52
|
-
els.unshift(root)
|
|
53
|
-
|
|
54
54
|
for (const el of els) {
|
|
55
55
|
const mEl = el as MicraElement
|
|
56
56
|
if (mEl.__micraEvents) continue
|
|
@@ -81,25 +81,19 @@ export function bindDataOn<S extends StateRecord>(
|
|
|
81
81
|
/**
|
|
82
82
|
* Bind `@event="method"` shorthand attributes (Stimulus-style).
|
|
83
83
|
* Bound once per element via `__micraAtBound` — re-renders are no-ops.
|
|
84
|
-
*
|
|
84
|
+
*
|
|
85
|
+
* @param els - Pre-computed list of elements with at least one @-prefixed attr
|
|
86
|
+
* (from scan.ts — replaces the old `querySelectorAll('*')` walk)
|
|
85
87
|
*
|
|
86
88
|
* @example
|
|
87
89
|
* <button @click="increment">+</button>
|
|
88
90
|
* <form @submit.prevent="handleSubmit">
|
|
89
91
|
*/
|
|
90
92
|
export function bindAtEvents<S extends StateRecord>(
|
|
91
|
-
|
|
93
|
+
els: Element[],
|
|
92
94
|
instance: InternalInstance<S>,
|
|
93
95
|
): void {
|
|
94
|
-
const
|
|
95
|
-
const all = isFragment
|
|
96
|
-
? queryAll(root as unknown as ParentNode, '*')
|
|
97
|
-
: queryOwnAll(root, '*')
|
|
98
|
-
|
|
99
|
-
// Include root itself for the regular-element case
|
|
100
|
-
if (!isFragment && !all.includes(root)) all.unshift(root)
|
|
101
|
-
|
|
102
|
-
for (const el of all) {
|
|
96
|
+
for (const el of els) {
|
|
103
97
|
const mEl = el as MicraElement
|
|
104
98
|
if (mEl.__micraAtBound) continue
|
|
105
99
|
|
|
@@ -133,25 +127,23 @@ export function bindAtEvents<S extends StateRecord>(
|
|
|
133
127
|
* Numeric inputs (`type="number"` / `type="range"`) write numbers, not strings.
|
|
134
128
|
* Checkbox inputs write booleans. Everything else writes strings.
|
|
135
129
|
*
|
|
130
|
+
* @param bindings - Pre-computed model bindings from scan.ts
|
|
131
|
+
* (each carries { el, expr } where expr is the state key)
|
|
132
|
+
*
|
|
136
133
|
* @example
|
|
137
134
|
* <input data-model="search"> // updates state.search on every keystroke
|
|
138
135
|
* <select data-model="sortBy"> // updates state.sortBy on change
|
|
139
136
|
*/
|
|
140
137
|
export function bindModels<S extends StateRecord>(
|
|
141
|
-
|
|
138
|
+
bindings: CachedBinding[],
|
|
142
139
|
instance: InternalInstance<S>,
|
|
143
140
|
): void {
|
|
144
|
-
const
|
|
145
|
-
const els = isFragment
|
|
146
|
-
? queryAll(root as unknown as ParentNode, '[data-model]')
|
|
147
|
-
: queryOwn(root, 'data-model')
|
|
148
|
-
|
|
149
|
-
for (const el of els) {
|
|
141
|
+
for (const { el, expr } of bindings) {
|
|
150
142
|
const mEl = el as MicraElement
|
|
151
143
|
if (mEl.__micraModel) continue
|
|
152
144
|
mEl.__micraModel = true
|
|
153
145
|
|
|
154
|
-
const key = (
|
|
146
|
+
const key = expr.trim()
|
|
155
147
|
const tag = el.tagName
|
|
156
148
|
const inputEl = el as HTMLInputElement
|
|
157
149
|
const inputType = inputEl.type
|
package/src/dom/refs.ts
CHANGED
|
@@ -2,33 +2,32 @@
|
|
|
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
|
|
|
12
11
|
import type { InternalInstance, MicraElement, StateRecord } from '../types'
|
|
13
|
-
import { queryOwn } from './query'
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
|
-
*
|
|
17
|
-
* `instance.refs`.
|
|
14
|
+
* Build `instance.refs` from the pre-scanned [data-ref] elements.
|
|
18
15
|
*
|
|
19
16
|
* Called once after the initial render and again on every re-render (refs may
|
|
20
17
|
* point to newly created elements after an each-list update).
|
|
21
18
|
*
|
|
19
|
+
* @param els - List of [data-ref] elements from scan.ts
|
|
20
|
+
*
|
|
22
21
|
* @example
|
|
23
22
|
* // HTML: <canvas data-ref="chart">
|
|
24
23
|
* // JS: this.refs.chart → HTMLCanvasElement
|
|
25
24
|
*/
|
|
26
25
|
export function collectRefs<S extends StateRecord>(
|
|
27
|
-
|
|
26
|
+
els: Element[],
|
|
28
27
|
instance: InternalInstance<S>,
|
|
29
28
|
): void {
|
|
30
29
|
instance.refs = {}
|
|
31
|
-
for (const el of
|
|
30
|
+
for (const el of els) {
|
|
32
31
|
const name = (el as MicraElement).dataset['ref']
|
|
33
32
|
if (name) instance.refs[name] = el as HTMLElement
|
|
34
33
|
}
|