micra.js 1.0.0 → 1.1.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 +2 -2
- package/dist/dom/events.d.ts +9 -4
- package/dist/micra.cjs.js +192 -47
- package/dist/micra.cjs.js.map +3 -3
- package/dist/micra.esm.js +192 -47
- package/dist/micra.esm.js.map +3 -3
- package/dist/micra.js +192 -47
- package/dist/micra.js.map +3 -3
- package/dist/micra.min.js +2 -2
- package/dist/types.d.ts +24 -3
- 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 +67 -24
- package/src/dom/each.ts +10 -1
- package/src/dom/events.ts +49 -19
- package/src/types.ts +26 -3
- package/src/utils/expr.ts +119 -7
package/dist/micra.esm.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Micra.js v1.
|
|
1
|
+
/* Micra.js v1.1.0 — https://github.com/micra-js/micra — MIT */
|
|
2
2
|
|
|
3
3
|
// src/utils/fetch.ts
|
|
4
4
|
function getCSRF() {
|
|
@@ -80,10 +80,69 @@ function debug() {
|
|
|
80
80
|
|
|
81
81
|
// src/utils/expr.ts
|
|
82
82
|
var exprCache = /* @__PURE__ */ new Map();
|
|
83
|
+
var warnedRuntime = /* @__PURE__ */ new Set();
|
|
83
84
|
var SIMPLE_PATH = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*$/;
|
|
85
|
+
var ALLOWED_GLOBALS = /* @__PURE__ */ new Set([
|
|
86
|
+
"Math",
|
|
87
|
+
"JSON",
|
|
88
|
+
"Date",
|
|
89
|
+
"String",
|
|
90
|
+
"Number",
|
|
91
|
+
"Boolean",
|
|
92
|
+
"Array",
|
|
93
|
+
"Object",
|
|
94
|
+
"parseInt",
|
|
95
|
+
"parseFloat",
|
|
96
|
+
"isNaN",
|
|
97
|
+
"isFinite",
|
|
98
|
+
"NaN",
|
|
99
|
+
"Infinity",
|
|
100
|
+
"undefined"
|
|
101
|
+
]);
|
|
102
|
+
var PARAM_S = "$s";
|
|
103
|
+
var PARAM_SAFE = "$safe";
|
|
104
|
+
var SAFE_OUTER = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
105
|
+
has(_target, key) {
|
|
106
|
+
if (typeof key !== "string") return false;
|
|
107
|
+
if (key === PARAM_S || key === PARAM_SAFE) return false;
|
|
108
|
+
return !ALLOWED_GLOBALS.has(key);
|
|
109
|
+
},
|
|
110
|
+
get() {
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
var safeWrapCache = /* @__PURE__ */ new WeakMap();
|
|
115
|
+
var OBJ_PROTO_KEYS = new Set(Object.getOwnPropertyNames(Object.prototype));
|
|
116
|
+
function safeStateWrap(state) {
|
|
117
|
+
const cached = safeWrapCache.get(state);
|
|
118
|
+
if (cached) return cached;
|
|
119
|
+
const wrapped = new Proxy(state, {
|
|
120
|
+
has(target, key) {
|
|
121
|
+
return safeStateHas(target, key);
|
|
122
|
+
},
|
|
123
|
+
get(target, key) {
|
|
124
|
+
return Reflect.get(target, key);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
safeWrapCache.set(state, wrapped);
|
|
128
|
+
return wrapped;
|
|
129
|
+
}
|
|
130
|
+
function safeStateHas(state, key) {
|
|
131
|
+
if (typeof key !== "string") return false;
|
|
132
|
+
if (!Reflect.has(state, key)) return false;
|
|
133
|
+
if (!OBJ_PROTO_KEYS.has(key)) return true;
|
|
134
|
+
let obj = state;
|
|
135
|
+
while (obj && obj !== Object.prototype) {
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) return true;
|
|
137
|
+
obj = Object.getPrototypeOf(obj);
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
84
141
|
function evalExpr(expr, state) {
|
|
85
142
|
if (SIMPLE_PATH.test(expr)) {
|
|
86
|
-
|
|
143
|
+
const parts = expr.split(".");
|
|
144
|
+
if (!safeStateHas(state, parts[0])) return void 0;
|
|
145
|
+
return parts.reduce(
|
|
87
146
|
(obj, key) => obj != null ? obj[key] : void 0,
|
|
88
147
|
state
|
|
89
148
|
);
|
|
@@ -92,7 +151,7 @@ function evalExpr(expr, state) {
|
|
|
92
151
|
try {
|
|
93
152
|
exprCache.set(
|
|
94
153
|
expr,
|
|
95
|
-
new Function("$s", `with($s){return (${expr})}`)
|
|
154
|
+
new Function("$s", "$safe", `with($safe){with($s){return (${expr})}}`)
|
|
96
155
|
);
|
|
97
156
|
} catch {
|
|
98
157
|
warn(`invalid expression "${expr}"`);
|
|
@@ -100,8 +159,12 @@ function evalExpr(expr, state) {
|
|
|
100
159
|
}
|
|
101
160
|
}
|
|
102
161
|
try {
|
|
103
|
-
return exprCache.get(expr)(state);
|
|
104
|
-
} catch {
|
|
162
|
+
return exprCache.get(expr)(safeStateWrap(state), SAFE_OUTER);
|
|
163
|
+
} catch (e) {
|
|
164
|
+
if (!warnedRuntime.has(expr)) {
|
|
165
|
+
warnedRuntime.add(expr);
|
|
166
|
+
warn(`runtime error in "${expr}": ${e.message}`);
|
|
167
|
+
}
|
|
105
168
|
return void 0;
|
|
106
169
|
}
|
|
107
170
|
}
|
|
@@ -117,8 +180,10 @@ function on(event, handler) {
|
|
|
117
180
|
return () => off(event, handler);
|
|
118
181
|
}
|
|
119
182
|
function off(event, handler) {
|
|
120
|
-
|
|
121
|
-
|
|
183
|
+
const set = _bus.get(event);
|
|
184
|
+
if (!set) return;
|
|
185
|
+
set.delete(handler);
|
|
186
|
+
if (set.size === 0) _bus.delete(event);
|
|
122
187
|
}
|
|
123
188
|
function emit(event, payload) {
|
|
124
189
|
var _a;
|
|
@@ -182,12 +247,8 @@ function applyHtml(el, expr, state) {
|
|
|
182
247
|
function applyIf(el, expr, state) {
|
|
183
248
|
el.style.display = evalExpr(expr, state) ? "" : "none";
|
|
184
249
|
}
|
|
185
|
-
function applyBind(el,
|
|
186
|
-
for (const
|
|
187
|
-
const colonIdx = pair.indexOf(":");
|
|
188
|
-
if (colonIdx === -1) continue;
|
|
189
|
-
const attr = pair.slice(0, colonIdx).trim();
|
|
190
|
-
const valExpr = pair.slice(colonIdx + 1).trim();
|
|
250
|
+
function applyBind(el, pairs, state) {
|
|
251
|
+
for (const [attr, valExpr] of pairs) {
|
|
191
252
|
const val = evalExpr(valExpr, state);
|
|
192
253
|
if (attr === "class") {
|
|
193
254
|
el.className = String(val != null ? val : "");
|
|
@@ -207,21 +268,28 @@ function applyBind(el, expr, state) {
|
|
|
207
268
|
}
|
|
208
269
|
}
|
|
209
270
|
}
|
|
210
|
-
function applyClass(el,
|
|
211
|
-
for (const
|
|
212
|
-
const colonIdx = pair.indexOf(":");
|
|
213
|
-
if (colonIdx === -1) continue;
|
|
214
|
-
const cls = pair.slice(0, colonIdx).trim();
|
|
215
|
-
const valExpr = pair.slice(colonIdx + 1).trim();
|
|
216
|
-
if (!cls) continue;
|
|
271
|
+
function applyClass(el, pairs, state) {
|
|
272
|
+
for (const [cls, valExpr] of pairs) {
|
|
217
273
|
el.classList.toggle(cls, Boolean(evalExpr(valExpr, state)));
|
|
218
274
|
}
|
|
219
275
|
}
|
|
276
|
+
function parsePairs(expr) {
|
|
277
|
+
const out = [];
|
|
278
|
+
for (const part of expr.split(",")) {
|
|
279
|
+
const colonIdx = part.indexOf(":");
|
|
280
|
+
if (colonIdx === -1) continue;
|
|
281
|
+
const left = part.slice(0, colonIdx).trim();
|
|
282
|
+
const right = part.slice(colonIdx + 1).trim();
|
|
283
|
+
if (!left) continue;
|
|
284
|
+
out.push([left, right]);
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
220
288
|
function applyModel(el, key, rawState) {
|
|
221
289
|
const html = el;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
290
|
+
const stateVal = rawState[key];
|
|
291
|
+
const desired = stateVal == null ? "" : String(stateVal);
|
|
292
|
+
if (html.value !== desired) html.value = desired;
|
|
225
293
|
}
|
|
226
294
|
function buildCache(root) {
|
|
227
295
|
const pick = (attr) => {
|
|
@@ -230,14 +298,15 @@ function buildCache(root) {
|
|
|
230
298
|
if ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, attr)) els.unshift(root);
|
|
231
299
|
return els.filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
|
|
232
300
|
};
|
|
301
|
+
const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
|
|
233
302
|
return {
|
|
234
303
|
text: pick("data-text"),
|
|
235
304
|
html: pick("data-html"),
|
|
236
305
|
if: pick("data-if"),
|
|
237
306
|
show: pick("data-show"),
|
|
238
|
-
bind:
|
|
307
|
+
bind: pickPairs("data-bind"),
|
|
239
308
|
model: pick("data-model"),
|
|
240
|
-
class:
|
|
309
|
+
class: pickPairs("data-class")
|
|
241
310
|
};
|
|
242
311
|
}
|
|
243
312
|
function applyDirectives(root, state, rawState, _instance) {
|
|
@@ -254,31 +323,52 @@ function applyFromList(cache, state, rawState) {
|
|
|
254
323
|
cache.html.forEach((b) => applyHtml(b.el, b.expr, state));
|
|
255
324
|
cache.if.forEach((b) => applyIf(b.el, b.expr, state));
|
|
256
325
|
cache.show.forEach((b) => applyIf(b.el, b.expr, state));
|
|
257
|
-
cache.bind.forEach((b) => applyBind(b.el, b.
|
|
326
|
+
cache.bind.forEach((b) => applyBind(b.el, b.pairs, state));
|
|
258
327
|
cache.model.forEach((b) => applyModel(b.el, b.expr.trim(), rawState));
|
|
259
|
-
cache.class.forEach((b) => applyClass(b.el, b.
|
|
328
|
+
cache.class.forEach((b) => applyClass(b.el, b.pairs, state));
|
|
260
329
|
}
|
|
261
330
|
function buildFragmentList(frag) {
|
|
262
331
|
const pick = (attr) => queryAll(frag, `[${attr}]`).filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
|
|
332
|
+
const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
|
|
263
333
|
return {
|
|
264
334
|
text: pick("data-text"),
|
|
265
335
|
html: pick("data-html"),
|
|
266
336
|
if: pick("data-if"),
|
|
267
337
|
show: pick("data-show"),
|
|
268
|
-
bind:
|
|
338
|
+
bind: pickPairs("data-bind"),
|
|
269
339
|
model: pick("data-model"),
|
|
270
|
-
class:
|
|
340
|
+
class: pickPairs("data-class")
|
|
271
341
|
};
|
|
272
342
|
}
|
|
273
343
|
function validateDirectives(root) {
|
|
344
|
+
var _a, _b;
|
|
274
345
|
queryOwn(root, "data-each").forEach((el) => {
|
|
275
|
-
|
|
346
|
+
const tmpl = el;
|
|
347
|
+
if (!el.hasAttribute("data-key") && !tmpl.__micraNoKeyWarned) {
|
|
348
|
+
tmpl.__micraNoKeyWarned = true;
|
|
276
349
|
warn(`data-each="${el.getAttribute("data-each")}" has no data-key \u2014 keyed diff disabled. Add data-key="id" for better performance.`);
|
|
277
350
|
}
|
|
278
351
|
});
|
|
352
|
+
const bindEls = queryOwn(root, "data-bind");
|
|
353
|
+
if (((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-bind")) && !bindEls.includes(root)) bindEls.unshift(root);
|
|
354
|
+
for (const el of bindEls) {
|
|
355
|
+
const spec = (_b = el.getAttribute("data-bind")) != null ? _b : "";
|
|
356
|
+
const hasClassBind = spec.split(",").some((p) => {
|
|
357
|
+
var _a2;
|
|
358
|
+
return ((_a2 = p.trim().split(":")[0]) == null ? void 0 : _a2.trim()) === "class";
|
|
359
|
+
});
|
|
360
|
+
if (hasClassBind && el.hasAttribute("data-class")) {
|
|
361
|
+
warn(`element has both data-bind="class:..." and data-class \u2014 they fight on every render. Use one.`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
279
364
|
}
|
|
280
365
|
|
|
281
366
|
// src/dom/events.ts
|
|
367
|
+
function track(instance, el, type, fn) {
|
|
368
|
+
var _a;
|
|
369
|
+
el.addEventListener(type, fn);
|
|
370
|
+
((_a = instance.__micraListeners) != null ? _a : instance.__micraListeners = []).push({ el, type, fn });
|
|
371
|
+
}
|
|
282
372
|
function bindDataOn(root, instance) {
|
|
283
373
|
var _a, _b;
|
|
284
374
|
const isFragment = root.nodeType === 11;
|
|
@@ -294,7 +384,7 @@ function bindDataOn(root, instance) {
|
|
|
294
384
|
const [evSpec, method] = part.trim().split(":");
|
|
295
385
|
if (!evSpec || !method) continue;
|
|
296
386
|
const [evName, ...mods] = evSpec.split(".");
|
|
297
|
-
el
|
|
387
|
+
track(instance, el, evName, (e) => {
|
|
298
388
|
if (mods.includes("prevent")) e.preventDefault();
|
|
299
389
|
if (mods.includes("stop")) e.stopPropagation();
|
|
300
390
|
if (mods.includes("self") && e.target !== el) return;
|
|
@@ -306,16 +396,18 @@ function bindDataOn(root, instance) {
|
|
|
306
396
|
}
|
|
307
397
|
}
|
|
308
398
|
function bindAtEvents(root, instance) {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const all = queryAll(root, "*");
|
|
399
|
+
const isFragment = root.nodeType === 11;
|
|
400
|
+
const all = isFragment ? queryAll(root, "*") : queryAll(root, "*");
|
|
401
|
+
if (!isFragment && !all.includes(root)) all.unshift(root);
|
|
313
402
|
for (const el of all) {
|
|
403
|
+
const mEl = el;
|
|
404
|
+
if (mEl.__micraAtBound) continue;
|
|
405
|
+
let bound = false;
|
|
314
406
|
for (const attr of Array.from(el.attributes)) {
|
|
315
407
|
if (!attr.name.startsWith("@")) continue;
|
|
316
408
|
const [evSpec, ...rest] = attr.name.slice(1).split(".");
|
|
317
409
|
const method = attr.value.trim();
|
|
318
|
-
el
|
|
410
|
+
track(instance, el, evSpec, (e) => {
|
|
319
411
|
if (rest.includes("prevent")) e.preventDefault();
|
|
320
412
|
if (rest.includes("stop")) e.stopPropagation();
|
|
321
413
|
if (rest.includes("self") && e.target !== el) return;
|
|
@@ -323,7 +415,9 @@ function bindAtEvents(root, instance) {
|
|
|
323
415
|
if (typeof fn === "function") fn.call(instance, e);
|
|
324
416
|
else warn(`method "${method}" not found`);
|
|
325
417
|
});
|
|
418
|
+
bound = true;
|
|
326
419
|
}
|
|
420
|
+
if (bound) mEl.__micraAtBound = true;
|
|
327
421
|
}
|
|
328
422
|
}
|
|
329
423
|
function bindModels(root, instance) {
|
|
@@ -336,14 +430,22 @@ function bindModels(root, instance) {
|
|
|
336
430
|
mEl.__micraModel = true;
|
|
337
431
|
const key = (_a = el.dataset["model"]) != null ? _a : "";
|
|
338
432
|
const tag = el.tagName;
|
|
433
|
+
const inputEl = el;
|
|
434
|
+
const inputType = inputEl.type;
|
|
339
435
|
const update = () => {
|
|
340
|
-
|
|
436
|
+
let val;
|
|
437
|
+
if (tag === "INPUT" && inputType === "checkbox") {
|
|
438
|
+
val = inputEl.checked;
|
|
439
|
+
} else if (tag === "INPUT" && (inputType === "number" || inputType === "range")) {
|
|
440
|
+
val = inputEl.value === "" ? null : inputEl.valueAsNumber;
|
|
441
|
+
} else {
|
|
442
|
+
val = inputEl.value;
|
|
443
|
+
}
|
|
444
|
+
;
|
|
341
445
|
instance.state[key] = val;
|
|
342
446
|
};
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
update
|
|
346
|
-
);
|
|
447
|
+
const evType = tag === "SELECT" || inputType === "radio" ? "change" : "input";
|
|
448
|
+
track(instance, el, evType, update);
|
|
347
449
|
}
|
|
348
450
|
}
|
|
349
451
|
|
|
@@ -382,9 +484,18 @@ function renderList(root, state, rawState, instance) {
|
|
|
382
484
|
function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
|
|
383
485
|
const nextKeys = /* @__PURE__ */ new Set();
|
|
384
486
|
const nextNodes = [];
|
|
487
|
+
let warnedNullKey = false;
|
|
488
|
+
let warnedDupKey = false;
|
|
385
489
|
for (const [index, item] of items.entries()) {
|
|
386
490
|
const key = item[keyAttr];
|
|
387
|
-
if (key == null
|
|
491
|
+
if (key == null && !warnedNullKey) {
|
|
492
|
+
warn(`data-key="${keyAttr}" is null/undefined on item at index ${index}`);
|
|
493
|
+
warnedNullKey = true;
|
|
494
|
+
}
|
|
495
|
+
if (nextKeys.has(key) && !warnedDupKey) {
|
|
496
|
+
warn(`data-key="${keyAttr}" has duplicate value ${JSON.stringify(key)} \u2014 rows will collide`);
|
|
497
|
+
warnedDupKey = true;
|
|
498
|
+
}
|
|
388
499
|
nextKeys.add(key);
|
|
389
500
|
let node = keyMap.get(key);
|
|
390
501
|
if (!node) {
|
|
@@ -487,15 +598,35 @@ function mount(selector, definition) {
|
|
|
487
598
|
let isRendering = false;
|
|
488
599
|
const schedule = createScheduler(() => instance.render());
|
|
489
600
|
instance.state = createReactiveState(rawState, schedule);
|
|
601
|
+
const boundMethods = /* @__PURE__ */ new Map();
|
|
490
602
|
const exprState = new Proxy(rawState, {
|
|
491
603
|
get(target, key) {
|
|
492
|
-
if (key
|
|
493
|
-
if (key
|
|
604
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) return target[key];
|
|
605
|
+
if (Object.prototype.hasOwnProperty.call(instance, key) && typeof instance[key] === "function") {
|
|
606
|
+
const cached = boundMethods.get(key);
|
|
607
|
+
if (cached) return cached;
|
|
608
|
+
const bound = instance[key].bind(instance);
|
|
609
|
+
boundMethods.set(key, bound);
|
|
610
|
+
return bound;
|
|
611
|
+
}
|
|
494
612
|
return void 0;
|
|
613
|
+
},
|
|
614
|
+
has(target, key) {
|
|
615
|
+
if (typeof key !== "string") return false;
|
|
616
|
+
if (Object.prototype.hasOwnProperty.call(target, key)) return true;
|
|
617
|
+
return Object.prototype.hasOwnProperty.call(instance, key) && typeof instance[key] === "function";
|
|
495
618
|
}
|
|
496
619
|
});
|
|
620
|
+
let warnedReentry = false;
|
|
497
621
|
instance.render = function() {
|
|
498
|
-
if (
|
|
622
|
+
if (instance.__micraDestroyed) return;
|
|
623
|
+
if (isRendering) {
|
|
624
|
+
if (!warnedReentry) {
|
|
625
|
+
warn("render() re-entry detected \u2014 mutation inside a directive expression is ignored. Move state writes to a method.");
|
|
626
|
+
warnedReentry = true;
|
|
627
|
+
}
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
499
630
|
isRendering = true;
|
|
500
631
|
try {
|
|
501
632
|
applyDirectives(root, exprState, rawState, instance);
|
|
@@ -509,8 +640,22 @@ function mount(selector, definition) {
|
|
|
509
640
|
}
|
|
510
641
|
};
|
|
511
642
|
instance.destroy = function() {
|
|
512
|
-
var _a2;
|
|
513
|
-
(
|
|
643
|
+
var _a2, _b;
|
|
644
|
+
if (instance.__micraDestroyed) return;
|
|
645
|
+
instance.__micraDestroyed = true;
|
|
646
|
+
(_a2 = instance.__micraListeners) == null ? void 0 : _a2.forEach(({ el, type, fn }) => el.removeEventListener(type, fn));
|
|
647
|
+
instance.__micraListeners = [];
|
|
648
|
+
const clearFlags = (el) => {
|
|
649
|
+
const m = el;
|
|
650
|
+
delete m.__micraEvents;
|
|
651
|
+
delete m.__micraAtBound;
|
|
652
|
+
delete m.__micraModel;
|
|
653
|
+
delete m.__micraCache;
|
|
654
|
+
};
|
|
655
|
+
clearFlags(root);
|
|
656
|
+
root.querySelectorAll("*").forEach(clearFlags);
|
|
657
|
+
(_b = instance.__micraSubs) == null ? void 0 : _b.forEach((unsub) => unsub());
|
|
658
|
+
instance.__micraSubs = [];
|
|
514
659
|
if (typeof definition.onDestroy === "function")
|
|
515
660
|
definition.onDestroy.call(instance);
|
|
516
661
|
_instances.delete(root);
|