micra.js 1.0.0 → 2.0.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/README.md +7 -4
- package/dist/dom/events.d.ts +9 -4
- package/dist/dom/query.d.ts +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/micra.cjs.js +219 -53
- package/dist/micra.cjs.js.map +3 -3
- package/dist/micra.esm.js +219 -53
- package/dist/micra.esm.js.map +3 -3
- package/dist/micra.js +219 -53
- package/dist/micra.js.map +3 -3
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +33 -4
- package/dist/utils/expr.d.ts +12 -1
- package/package.json +2 -2
- package/src/core/bus.ts +4 -1
- package/src/core/mount.ts +54 -3
- package/src/dom/directives.ts +107 -29
- package/src/dom/each.ts +14 -2
- package/src/dom/events.ts +50 -20
- package/src/dom/query.ts +15 -1
- package/src/index.ts +1 -1
- package/src/types.ts +36 -4
- package/src/utils/expr.ts +119 -7
- package/src/utils/fetch.ts +2 -2
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Use it when you want:
|
|
|
27
27
|
<button @click="increment">+</button>
|
|
28
28
|
</div>
|
|
29
29
|
|
|
30
|
-
<script src="https://
|
|
30
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js/dist/micra.min.js"></script>
|
|
31
31
|
<script>
|
|
32
32
|
Micra.define('counter', {
|
|
33
33
|
state: { count: 0 },
|
|
@@ -44,7 +44,7 @@ Use it when you want:
|
|
|
44
44
|
### CDN
|
|
45
45
|
|
|
46
46
|
```html
|
|
47
|
-
<script src="https://
|
|
47
|
+
<script src="https://cdn.jsdelivr.net/npm/micra.js/dist/micra.min.js"></script>
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
### npm
|
|
@@ -54,7 +54,7 @@ npm install micra.js
|
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
|
-
import * as Micra from 'micra'
|
|
57
|
+
import * as Micra from 'micra.js'
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
## Basic usage
|
|
@@ -71,7 +71,7 @@ A counter mounted automatically from `data-component`:
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
|
-
import * as Micra from 'micra'
|
|
74
|
+
import * as Micra from 'micra.js'
|
|
75
75
|
|
|
76
76
|
Micra.define('counter', {
|
|
77
77
|
state: { count: 0 },
|
|
@@ -164,3 +164,6 @@ this.on(event, handler)
|
|
|
164
164
|
- [SSR](./docs/ssr.md)
|
|
165
165
|
- [Examples](./docs/examples.md)
|
|
166
166
|
- [API reference](./docs/api-reference.md)
|
|
167
|
+
- Recipes:
|
|
168
|
+
- [Todo app](./docs/recipes/todo-app.md)
|
|
169
|
+
- [Server-sent events (SSE)](./docs/recipes/sse.md)
|
package/dist/dom/events.d.ts
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Responsibilities:
|
|
5
5
|
* - Bind `data-on="event:method"` listeners (once per element)
|
|
6
|
-
* - Bind `@event="method"` shorthand (
|
|
6
|
+
* - Bind `@event="method"` shorthand (once per element)
|
|
7
|
+
* - Bind `data-model` two-way input listeners (once per element)
|
|
7
8
|
*
|
|
8
|
-
* LLM NOTE:
|
|
9
|
-
*
|
|
9
|
+
* LLM NOTE: Every listener attached here is also recorded in
|
|
10
|
+
* instance.__micraListeners so destroy() can remove it cleanly.
|
|
11
|
+
* Re-render skips already-bound elements via per-element __micra* flags.
|
|
10
12
|
*/
|
|
11
13
|
import type { InternalInstance, StateRecord } from '../types';
|
|
12
14
|
/**
|
|
@@ -22,7 +24,7 @@ import type { InternalInstance, StateRecord } from '../types';
|
|
|
22
24
|
export declare function bindDataOn<S extends StateRecord>(root: Element, instance: InternalInstance<S>): void;
|
|
23
25
|
/**
|
|
24
26
|
* Bind `@event="method"` shorthand attributes (Stimulus-style).
|
|
25
|
-
*
|
|
27
|
+
* Bound once per element via `__micraAtBound` — re-renders are no-ops.
|
|
26
28
|
* Supports the same modifiers as data-on: `@click.prevent="submit"`.
|
|
27
29
|
*
|
|
28
30
|
* @example
|
|
@@ -34,6 +36,9 @@ export declare function bindAtEvents<S extends StateRecord>(root: Element, insta
|
|
|
34
36
|
* Two-way binding: `data-model="key"` wires <input>/<select>/<textarea>
|
|
35
37
|
* to `state[key]`. Binding is attached once per element.
|
|
36
38
|
*
|
|
39
|
+
* Numeric inputs (`type="number"` / `type="range"`) write numbers, not strings.
|
|
40
|
+
* Checkbox inputs write booleans. Everything else writes strings.
|
|
41
|
+
*
|
|
37
42
|
* @example
|
|
38
43
|
* <input data-model="search"> // updates state.search on every keystroke
|
|
39
44
|
* <select data-model="sortBy"> // updates state.sortBy on change
|
package/dist/dom/query.d.ts
CHANGED
|
@@ -21,3 +21,9 @@ export declare function queryAll(root: ParentNode, sel: string): Element[];
|
|
|
21
21
|
* owned by that nested component, not by root's component — so we skip it.
|
|
22
22
|
*/
|
|
23
23
|
export declare function queryOwn(root: Element, attr: string): Element[];
|
|
24
|
+
/**
|
|
25
|
+
* Like queryOwn but accepts an arbitrary CSS selector. Used by bindAtEvents
|
|
26
|
+
* which scans `*` for `@`-prefixed attribute names (no attribute selector exists
|
|
27
|
+
* for those).
|
|
28
|
+
*/
|
|
29
|
+
export declare function queryOwnAll(root: Element, sel: string): Element[];
|
package/dist/index.d.ts
CHANGED
package/dist/micra.cjs.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Micra.js
|
|
1
|
+
/* Micra.js v2.0.0 — https://github.com/micra-js/micra — MIT */
|
|
2
2
|
"use strict";
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -66,9 +66,9 @@ async function micraFetch(url, options = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
if (Object.keys(params).length)
|
|
68
68
|
finalUrl += (url.includes("?") ? "&" : "?") + new URLSearchParams(params);
|
|
69
|
-
} else {
|
|
69
|
+
} else if (options.body !== void 0) {
|
|
70
70
|
headers["Content-Type"] = "application/json";
|
|
71
|
-
body = JSON.stringify(options.body
|
|
71
|
+
body = JSON.stringify(options.body);
|
|
72
72
|
}
|
|
73
73
|
const res = await fetch(finalUrl, {
|
|
74
74
|
method,
|
|
@@ -115,10 +115,69 @@ function debug() {
|
|
|
115
115
|
|
|
116
116
|
// src/utils/expr.ts
|
|
117
117
|
var exprCache = /* @__PURE__ */ new Map();
|
|
118
|
+
var warnedRuntime = /* @__PURE__ */ new Set();
|
|
118
119
|
var SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/;
|
|
120
|
+
var ALLOWED_GLOBALS = /* @__PURE__ */ new Set([
|
|
121
|
+
"Math",
|
|
122
|
+
"JSON",
|
|
123
|
+
"Date",
|
|
124
|
+
"String",
|
|
125
|
+
"Number",
|
|
126
|
+
"Boolean",
|
|
127
|
+
"Array",
|
|
128
|
+
"Object",
|
|
129
|
+
"parseInt",
|
|
130
|
+
"parseFloat",
|
|
131
|
+
"isNaN",
|
|
132
|
+
"isFinite",
|
|
133
|
+
"NaN",
|
|
134
|
+
"Infinity",
|
|
135
|
+
"undefined"
|
|
136
|
+
]);
|
|
137
|
+
var PARAM_S = "$s";
|
|
138
|
+
var PARAM_SAFE = "$safe";
|
|
139
|
+
var SAFE_OUTER = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
140
|
+
has(_target, key) {
|
|
141
|
+
if (typeof key !== "string") return false;
|
|
142
|
+
if (key === PARAM_S || key === PARAM_SAFE) return false;
|
|
143
|
+
return !ALLOWED_GLOBALS.has(key);
|
|
144
|
+
},
|
|
145
|
+
get() {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
var safeWrapCache = /* @__PURE__ */ new WeakMap();
|
|
150
|
+
var OBJ_PROTO_KEYS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
151
|
+
function safeStateWrap(state) {
|
|
152
|
+
const cached = safeWrapCache.get(state);
|
|
153
|
+
if (cached) return cached;
|
|
154
|
+
const wrapped = new Proxy(state, {
|
|
155
|
+
has(target, key) {
|
|
156
|
+
return safeStateHas(target, key);
|
|
157
|
+
},
|
|
158
|
+
get(target, key) {
|
|
159
|
+
return Reflect.get(target, key);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
safeWrapCache.set(state, wrapped);
|
|
163
|
+
return wrapped;
|
|
164
|
+
}
|
|
165
|
+
function safeStateHas(state, key) {
|
|
166
|
+
if (typeof key !== "string") return false;
|
|
167
|
+
if (!Reflect.has(state, key)) return false;
|
|
168
|
+
if (!OBJ_PROTO_KEYS.has(key)) return true;
|
|
169
|
+
let obj = state;
|
|
170
|
+
while (obj && obj !== Object.prototype) {
|
|
171
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) return true;
|
|
172
|
+
obj = Object.getPrototypeOf(obj);
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
119
176
|
function evalExpr(expr, state) {
|
|
120
177
|
if (SIMPLE_PATH.test(expr)) {
|
|
121
|
-
|
|
178
|
+
const parts = expr.split(".");
|
|
179
|
+
if (!safeStateHas(state, parts[0])) return void 0;
|
|
180
|
+
return parts.reduce(
|
|
122
181
|
(obj, key) => obj != null ? obj[key] : void 0,
|
|
123
182
|
state
|
|
124
183
|
);
|
|
@@ -127,7 +186,7 @@ function evalExpr(expr, state) {
|
|
|
127
186
|
try {
|
|
128
187
|
exprCache.set(
|
|
129
188
|
expr,
|
|
130
|
-
new Function("$s", `with($s){return (${expr})}`)
|
|
189
|
+
new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
|
|
131
190
|
);
|
|
132
191
|
} catch {
|
|
133
192
|
warn(`invalid expression "${expr}"`);
|
|
@@ -135,8 +194,12 @@ function evalExpr(expr, state) {
|
|
|
135
194
|
}
|
|
136
195
|
}
|
|
137
196
|
try {
|
|
138
|
-
return exprCache.get(expr)(state);
|
|
139
|
-
} catch {
|
|
197
|
+
return exprCache.get(expr)(safeStateWrap(state), SAFE_OUTER);
|
|
198
|
+
} catch (e) {
|
|
199
|
+
if (!warnedRuntime.has(expr)) {
|
|
200
|
+
warnedRuntime.add(expr);
|
|
201
|
+
warn(`runtime error in "${expr}": ${e.message}`);
|
|
202
|
+
}
|
|
140
203
|
return void 0;
|
|
141
204
|
}
|
|
142
205
|
}
|
|
@@ -152,8 +215,10 @@ function on(event, handler) {
|
|
|
152
215
|
return () => off(event, handler);
|
|
153
216
|
}
|
|
154
217
|
function off(event, handler) {
|
|
155
|
-
|
|
156
|
-
|
|
218
|
+
const set = _bus.get(event);
|
|
219
|
+
if (!set) return;
|
|
220
|
+
set.delete(handler);
|
|
221
|
+
if (set.size === 0) _bus.delete(event);
|
|
157
222
|
}
|
|
158
223
|
function emit(event, payload) {
|
|
159
224
|
var _a;
|
|
@@ -194,7 +259,13 @@ function queryAll(root, sel) {
|
|
|
194
259
|
return Array.from(root.querySelectorAll(sel));
|
|
195
260
|
}
|
|
196
261
|
function queryOwn(root, attr) {
|
|
197
|
-
return queryAll(root, `[${attr}]`)
|
|
262
|
+
return filterOwn(root, queryAll(root, `[${attr}]`));
|
|
263
|
+
}
|
|
264
|
+
function queryOwnAll(root, sel) {
|
|
265
|
+
return filterOwn(root, queryAll(root, sel));
|
|
266
|
+
}
|
|
267
|
+
function filterOwn(root, els) {
|
|
268
|
+
return els.filter((el) => {
|
|
198
269
|
let node = el.parentElement;
|
|
199
270
|
while (node && node !== root) {
|
|
200
271
|
if (node.hasAttribute("data-component")) return false;
|
|
@@ -214,15 +285,25 @@ function applyHtml(el, expr, state) {
|
|
|
214
285
|
var _a;
|
|
215
286
|
el.innerHTML = String((_a = evalExpr(expr, state)) != null ? _a : "");
|
|
216
287
|
}
|
|
217
|
-
function applyIf(
|
|
288
|
+
function applyIf(binding, state) {
|
|
289
|
+
const el = binding.el;
|
|
290
|
+
const truthy = !!evalExpr(binding.expr, state);
|
|
291
|
+
if (truthy) {
|
|
292
|
+
const ph = binding.placeholder;
|
|
293
|
+
if (ph && ph.parentNode) ph.parentNode.replaceChild(el, ph);
|
|
294
|
+
} else {
|
|
295
|
+
const parent = el.parentNode;
|
|
296
|
+
if (parent) {
|
|
297
|
+
if (!binding.placeholder) binding.placeholder = document.createComment("if");
|
|
298
|
+
parent.replaceChild(binding.placeholder, el);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function applyShow(el, expr, state) {
|
|
218
303
|
el.style.display = evalExpr(expr, state) ? "" : "none";
|
|
219
304
|
}
|
|
220
|
-
function applyBind(el,
|
|
221
|
-
for (const
|
|
222
|
-
const colonIdx = pair.indexOf(":");
|
|
223
|
-
if (colonIdx === -1) continue;
|
|
224
|
-
const attr = pair.slice(0, colonIdx).trim();
|
|
225
|
-
const valExpr = pair.slice(colonIdx + 1).trim();
|
|
305
|
+
function applyBind(el, pairs, state) {
|
|
306
|
+
for (const [attr, valExpr] of pairs) {
|
|
226
307
|
const val = evalExpr(valExpr, state);
|
|
227
308
|
if (attr === "class") {
|
|
228
309
|
el.className = String(val != null ? val : "");
|
|
@@ -242,21 +323,28 @@ function applyBind(el, expr, state) {
|
|
|
242
323
|
}
|
|
243
324
|
}
|
|
244
325
|
}
|
|
245
|
-
function applyClass(el,
|
|
246
|
-
for (const
|
|
247
|
-
const colonIdx = pair.indexOf(":");
|
|
248
|
-
if (colonIdx === -1) continue;
|
|
249
|
-
const cls = pair.slice(0, colonIdx).trim();
|
|
250
|
-
const valExpr = pair.slice(colonIdx + 1).trim();
|
|
251
|
-
if (!cls) continue;
|
|
326
|
+
function applyClass(el, pairs, state) {
|
|
327
|
+
for (const [cls, valExpr] of pairs) {
|
|
252
328
|
el.classList.toggle(cls, Boolean(evalExpr(valExpr, state)));
|
|
253
329
|
}
|
|
254
330
|
}
|
|
331
|
+
function parsePairs(expr) {
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const part of expr.split(",")) {
|
|
334
|
+
const colonIdx = part.indexOf(":");
|
|
335
|
+
if (colonIdx === -1) continue;
|
|
336
|
+
const left = part.slice(0, colonIdx).trim();
|
|
337
|
+
const right = part.slice(colonIdx + 1).trim();
|
|
338
|
+
if (!left) continue;
|
|
339
|
+
out.push([left, right]);
|
|
340
|
+
}
|
|
341
|
+
return out;
|
|
342
|
+
}
|
|
255
343
|
function applyModel(el, key, rawState) {
|
|
256
344
|
const html = el;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
345
|
+
const stateVal = rawState[key];
|
|
346
|
+
const desired = stateVal == null ? "" : String(stateVal);
|
|
347
|
+
if (html.value !== desired) html.value = desired;
|
|
260
348
|
}
|
|
261
349
|
function buildCache(root) {
|
|
262
350
|
const pick = (attr) => {
|
|
@@ -265,14 +353,15 @@ function buildCache(root) {
|
|
|
265
353
|
if ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, attr)) els.unshift(root);
|
|
266
354
|
return els.filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
|
|
267
355
|
};
|
|
356
|
+
const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
|
|
268
357
|
return {
|
|
269
358
|
text: pick("data-text"),
|
|
270
359
|
html: pick("data-html"),
|
|
271
360
|
if: pick("data-if"),
|
|
272
361
|
show: pick("data-show"),
|
|
273
|
-
bind:
|
|
362
|
+
bind: pickPairs("data-bind"),
|
|
274
363
|
model: pick("data-model"),
|
|
275
|
-
class:
|
|
364
|
+
class: pickPairs("data-class")
|
|
276
365
|
};
|
|
277
366
|
}
|
|
278
367
|
function applyDirectives(root, state, rawState, _instance) {
|
|
@@ -285,35 +374,56 @@ function applyDirectives(root, state, rawState, _instance) {
|
|
|
285
374
|
applyFromList(el.__micraCache, state, rawState);
|
|
286
375
|
}
|
|
287
376
|
function applyFromList(cache, state, rawState) {
|
|
377
|
+
cache.if.forEach((b) => applyIf(b, state));
|
|
288
378
|
cache.text.forEach((b) => applyText(b.el, b.expr, state));
|
|
289
379
|
cache.html.forEach((b) => applyHtml(b.el, b.expr, state));
|
|
290
|
-
cache.
|
|
291
|
-
cache.
|
|
292
|
-
cache.bind.forEach((b) => applyBind(b.el, b.expr, state));
|
|
380
|
+
cache.show.forEach((b) => applyShow(b.el, b.expr, state));
|
|
381
|
+
cache.bind.forEach((b) => applyBind(b.el, b.pairs, state));
|
|
293
382
|
cache.model.forEach((b) => applyModel(b.el, b.expr.trim(), rawState));
|
|
294
|
-
cache.class.forEach((b) => applyClass(b.el, b.
|
|
383
|
+
cache.class.forEach((b) => applyClass(b.el, b.pairs, state));
|
|
295
384
|
}
|
|
296
385
|
function buildFragmentList(frag) {
|
|
297
386
|
const pick = (attr) => queryAll(frag, `[${attr}]`).filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
|
|
387
|
+
const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
|
|
298
388
|
return {
|
|
299
389
|
text: pick("data-text"),
|
|
300
390
|
html: pick("data-html"),
|
|
301
391
|
if: pick("data-if"),
|
|
302
392
|
show: pick("data-show"),
|
|
303
|
-
bind:
|
|
393
|
+
bind: pickPairs("data-bind"),
|
|
304
394
|
model: pick("data-model"),
|
|
305
|
-
class:
|
|
395
|
+
class: pickPairs("data-class")
|
|
306
396
|
};
|
|
307
397
|
}
|
|
308
398
|
function validateDirectives(root) {
|
|
399
|
+
var _a, _b;
|
|
309
400
|
queryOwn(root, "data-each").forEach((el) => {
|
|
310
|
-
|
|
401
|
+
const tmpl = el;
|
|
402
|
+
if (!el.hasAttribute("data-key") && !tmpl.__micraNoKeyWarned) {
|
|
403
|
+
tmpl.__micraNoKeyWarned = true;
|
|
311
404
|
warn(`data-each="${el.getAttribute("data-each")}" has no data-key \u2014 keyed diff disabled. Add data-key="id" for better performance.`);
|
|
312
405
|
}
|
|
313
406
|
});
|
|
407
|
+
const bindEls = queryOwn(root, "data-bind");
|
|
408
|
+
if (((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-bind")) && !bindEls.includes(root)) bindEls.unshift(root);
|
|
409
|
+
for (const el of bindEls) {
|
|
410
|
+
const spec = (_b = el.getAttribute("data-bind")) != null ? _b : "";
|
|
411
|
+
const hasClassBind = spec.split(",").some((p) => {
|
|
412
|
+
var _a2;
|
|
413
|
+
return ((_a2 = p.trim().split(":")[0]) == null ? void 0 : _a2.trim()) === "class";
|
|
414
|
+
});
|
|
415
|
+
if (hasClassBind && el.hasAttribute("data-class")) {
|
|
416
|
+
warn(`element has both data-bind="class:..." and data-class \u2014 they fight on every render. Use one.`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
314
419
|
}
|
|
315
420
|
|
|
316
421
|
// src/dom/events.ts
|
|
422
|
+
function track(instance, el, type, fn) {
|
|
423
|
+
var _a;
|
|
424
|
+
el.addEventListener(type, fn);
|
|
425
|
+
((_a = instance.__micraListeners) != null ? _a : instance.__micraListeners = []).push({ el, type, fn });
|
|
426
|
+
}
|
|
317
427
|
function bindDataOn(root, instance) {
|
|
318
428
|
var _a, _b;
|
|
319
429
|
const isFragment = root.nodeType === 11;
|
|
@@ -329,7 +439,7 @@ function bindDataOn(root, instance) {
|
|
|
329
439
|
const [evSpec, method] = part.trim().split(":");
|
|
330
440
|
if (!evSpec || !method) continue;
|
|
331
441
|
const [evName, ...mods] = evSpec.split(".");
|
|
332
|
-
el
|
|
442
|
+
track(instance, el, evName, (e) => {
|
|
333
443
|
if (mods.includes("prevent")) e.preventDefault();
|
|
334
444
|
if (mods.includes("stop")) e.stopPropagation();
|
|
335
445
|
if (mods.includes("self") && e.target !== el) return;
|
|
@@ -341,16 +451,18 @@ function bindDataOn(root, instance) {
|
|
|
341
451
|
}
|
|
342
452
|
}
|
|
343
453
|
function bindAtEvents(root, instance) {
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const all = queryAll(root, "*");
|
|
454
|
+
const isFragment = root.nodeType === 11;
|
|
455
|
+
const all = isFragment ? queryAll(root, "*") : queryOwnAll(root, "*");
|
|
456
|
+
if (!isFragment && !all.includes(root)) all.unshift(root);
|
|
348
457
|
for (const el of all) {
|
|
458
|
+
const mEl = el;
|
|
459
|
+
if (mEl.__micraAtBound) continue;
|
|
460
|
+
let bound = false;
|
|
349
461
|
for (const attr of Array.from(el.attributes)) {
|
|
350
462
|
if (!attr.name.startsWith("@")) continue;
|
|
351
463
|
const [evSpec, ...rest] = attr.name.slice(1).split(".");
|
|
352
464
|
const method = attr.value.trim();
|
|
353
|
-
el
|
|
465
|
+
track(instance, el, evSpec, (e) => {
|
|
354
466
|
if (rest.includes("prevent")) e.preventDefault();
|
|
355
467
|
if (rest.includes("stop")) e.stopPropagation();
|
|
356
468
|
if (rest.includes("self") && e.target !== el) return;
|
|
@@ -358,7 +470,9 @@ function bindAtEvents(root, instance) {
|
|
|
358
470
|
if (typeof fn === "function") fn.call(instance, e);
|
|
359
471
|
else warn(`method "${method}" not found`);
|
|
360
472
|
});
|
|
473
|
+
bound = true;
|
|
361
474
|
}
|
|
475
|
+
if (bound) mEl.__micraAtBound = true;
|
|
362
476
|
}
|
|
363
477
|
}
|
|
364
478
|
function bindModels(root, instance) {
|
|
@@ -371,14 +485,22 @@ function bindModels(root, instance) {
|
|
|
371
485
|
mEl.__micraModel = true;
|
|
372
486
|
const key = (_a = el.dataset["model"]) != null ? _a : "";
|
|
373
487
|
const tag = el.tagName;
|
|
488
|
+
const inputEl = el;
|
|
489
|
+
const inputType = inputEl.type;
|
|
374
490
|
const update = () => {
|
|
375
|
-
|
|
491
|
+
let val;
|
|
492
|
+
if (tag === "INPUT" && inputType === "checkbox") {
|
|
493
|
+
val = inputEl.checked;
|
|
494
|
+
} else if (tag === "INPUT" && (inputType === "number" || inputType === "range")) {
|
|
495
|
+
val = inputEl.value === "" ? null : inputEl.valueAsNumber;
|
|
496
|
+
} else {
|
|
497
|
+
val = inputEl.value;
|
|
498
|
+
}
|
|
499
|
+
;
|
|
376
500
|
instance.state[key] = val;
|
|
377
501
|
};
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
update
|
|
381
|
-
);
|
|
502
|
+
const evType = tag === "SELECT" || inputType === "radio" ? "change" : "input";
|
|
503
|
+
track(instance, el, evType, update);
|
|
382
504
|
}
|
|
383
505
|
}
|
|
384
506
|
|
|
@@ -401,6 +523,7 @@ function renderList(root, state, rawState, instance) {
|
|
|
401
523
|
const marker = tmpl.__micraMarker;
|
|
402
524
|
const keyMap = tmpl.__micraNodes;
|
|
403
525
|
const parent = marker.parentNode;
|
|
526
|
+
if (!parent) return;
|
|
404
527
|
if (!Array.isArray(items)) {
|
|
405
528
|
tmpl.__micraList.forEach((n) => n.remove());
|
|
406
529
|
tmpl.__micraList = [];
|
|
@@ -417,9 +540,18 @@ function renderList(root, state, rawState, instance) {
|
|
|
417
540
|
function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
|
|
418
541
|
const nextKeys = /* @__PURE__ */ new Set();
|
|
419
542
|
const nextNodes = [];
|
|
543
|
+
let warnedNullKey = false;
|
|
544
|
+
let warnedDupKey = false;
|
|
420
545
|
for (const [index, item] of items.entries()) {
|
|
421
546
|
const key = item[keyAttr];
|
|
422
|
-
if (key == null
|
|
547
|
+
if (key == null && !warnedNullKey) {
|
|
548
|
+
warn(`data-key="${keyAttr}" is null/undefined on item at index ${index}`);
|
|
549
|
+
warnedNullKey = true;
|
|
550
|
+
}
|
|
551
|
+
if (nextKeys.has(key) && !warnedDupKey) {
|
|
552
|
+
warn(`data-key="${keyAttr}" has duplicate value ${JSON.stringify(key)} \u2014 rows will collide`);
|
|
553
|
+
warnedDupKey = true;
|
|
554
|
+
}
|
|
423
555
|
nextKeys.add(key);
|
|
424
556
|
let node = keyMap.get(key);
|
|
425
557
|
if (!node) {
|
|
@@ -522,15 +654,35 @@ function mount(selector, definition) {
|
|
|
522
654
|
let isRendering = false;
|
|
523
655
|
const schedule = createScheduler(() => instance.render());
|
|
524
656
|
instance.state = createReactiveState(rawState, schedule);
|
|
657
|
+
const boundMethods = /* @__PURE__ */ new Map();
|
|
525
658
|
const exprState = new Proxy(rawState, {
|
|
526
659
|
get(target, key) {
|
|
527
|
-
if (key
|
|
528
|
-
if (key
|
|
660
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) return target[key];
|
|
661
|
+
if (Object.prototype.hasOwnProperty.call(instance, key) && typeof instance[key] === "function") {
|
|
662
|
+
const cached = boundMethods.get(key);
|
|
663
|
+
if (cached) return cached;
|
|
664
|
+
const bound = instance[key].bind(instance);
|
|
665
|
+
boundMethods.set(key, bound);
|
|
666
|
+
return bound;
|
|
667
|
+
}
|
|
529
668
|
return void 0;
|
|
669
|
+
},
|
|
670
|
+
has(target, key) {
|
|
671
|
+
if (typeof key !== "string") return false;
|
|
672
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) return true;
|
|
673
|
+
return Object.prototype.hasOwnProperty.call(instance, key) && typeof instance[key] === "function";
|
|
530
674
|
}
|
|
531
675
|
});
|
|
676
|
+
let warnedReentry = false;
|
|
532
677
|
instance.render = function() {
|
|
533
|
-
if (
|
|
678
|
+
if (instance.__micraDestroyed) return;
|
|
679
|
+
if (isRendering) {
|
|
680
|
+
if (!warnedReentry) {
|
|
681
|
+
warn("render() re-entry detected \u2014 mutation inside a directive expression is ignored. Move state writes to a method.");
|
|
682
|
+
warnedReentry = true;
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
534
686
|
isRendering = true;
|
|
535
687
|
try {
|
|
536
688
|
applyDirectives(root, exprState, rawState, instance);
|
|
@@ -544,8 +696,22 @@ function mount(selector, definition) {
|
|
|
544
696
|
}
|
|
545
697
|
};
|
|
546
698
|
instance.destroy = function() {
|
|
547
|
-
var _a2;
|
|
548
|
-
(
|
|
699
|
+
var _a2, _b;
|
|
700
|
+
if (instance.__micraDestroyed) return;
|
|
701
|
+
instance.__micraDestroyed = true;
|
|
702
|
+
(_a2 = instance.__micraListeners) == null ? void 0 : _a2.forEach(({ el, type, fn }) => el.removeEventListener(type, fn));
|
|
703
|
+
instance.__micraListeners = [];
|
|
704
|
+
const clearFlags = (el) => {
|
|
705
|
+
const m = el;
|
|
706
|
+
delete m.__micraEvents;
|
|
707
|
+
delete m.__micraAtBound;
|
|
708
|
+
delete m.__micraModel;
|
|
709
|
+
delete m.__micraCache;
|
|
710
|
+
};
|
|
711
|
+
clearFlags(root);
|
|
712
|
+
root.querySelectorAll("*").forEach(clearFlags);
|
|
713
|
+
(_b = instance.__micraSubs) == null ? void 0 : _b.forEach((unsub) => unsub());
|
|
714
|
+
instance.__micraSubs = [];
|
|
549
715
|
if (typeof definition.onDestroy === "function")
|
|
550
716
|
definition.onDestroy.call(instance);
|
|
551
717
|
_instances.delete(root);
|