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/dist/micra.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v1.0.0 — https://github.com/micra-js/micra — MIT */
1
+ /* Micra.js v2.0.0 — https://github.com/micra-js/micra — MIT */
2
2
  "use strict";
3
3
  var Micra = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -66,9 +66,9 @@ var Micra = (() => {
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 !== void 0 ? options.body : options);
71
+ body = JSON.stringify(options.body);
72
72
  }
73
73
  const res = await fetch(finalUrl, {
74
74
  method,
@@ -115,10 +115,69 @@ var Micra = (() => {
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
- return expr.split(".").reduce(
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 @@ var Micra = (() => {
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 @@ var Micra = (() => {
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 @@ var Micra = (() => {
152
215
  return () => off(event, handler);
153
216
  }
154
217
  function off(event, handler) {
155
- var _a;
156
- (_a = _bus.get(event)) == null ? void 0 : _a.delete(handler);
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 @@ var Micra = (() => {
194
259
  return Array.from(root.querySelectorAll(sel));
195
260
  }
196
261
  function queryOwn(root, attr) {
197
- return queryAll(root, `[${attr}]`).filter((el) => {
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 @@ var Micra = (() => {
214
285
  var _a;
215
286
  el.innerHTML = String((_a = evalExpr(expr, state)) != null ? _a : "");
216
287
  }
217
- function applyIf(el, expr, state) {
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, expr, state) {
221
- for (const pair of expr.split(",")) {
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 @@ var Micra = (() => {
242
323
  }
243
324
  }
244
325
  }
245
- function applyClass(el, expr, state) {
246
- for (const pair of expr.split(",")) {
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
- if (document.activeElement !== el) {
258
- html.value = rawState[key] == null ? "" : String(rawState[key]);
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 @@ var Micra = (() => {
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: pick("data-bind"),
362
+ bind: pickPairs("data-bind"),
274
363
  model: pick("data-model"),
275
- class: pick("data-class")
364
+ class: pickPairs("data-class")
276
365
  };
277
366
  }
278
367
  function applyDirectives(root, state, rawState, _instance) {
@@ -285,35 +374,56 @@ var Micra = (() => {
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.if.forEach((b) => applyIf(b.el, b.expr, state));
291
- cache.show.forEach((b) => applyIf(b.el, b.expr, state));
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.expr, state));
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: pick("data-bind"),
393
+ bind: pickPairs("data-bind"),
304
394
  model: pick("data-model"),
305
- class: pick("data-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
- if (!el.hasAttribute("data-key")) {
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 @@ var Micra = (() => {
329
439
  const [evSpec, method] = part.trim().split(":");
330
440
  if (!evSpec || !method) continue;
331
441
  const [evName, ...mods] = evSpec.split(".");
332
- el.addEventListener(evName, (e) => {
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 @@ var Micra = (() => {
341
451
  }
342
452
  }
343
453
  function bindAtEvents(root, instance) {
344
- const mRoot = root;
345
- if (mRoot.__micraAtScanned) return;
346
- mRoot.__micraAtScanned = true;
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.addEventListener(evSpec, (e) => {
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 @@ var Micra = (() => {
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 @@ var Micra = (() => {
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
- const val = tag === "INPUT" && el.type === "checkbox" ? el.checked : el.value;
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
- el.addEventListener(
379
- tag === "SELECT" || el.type === "radio" ? "change" : "input",
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 @@ var Micra = (() => {
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 @@ var Micra = (() => {
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) warn(`data-key="${keyAttr}" is null/undefined on item at index ${index}`);
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 @@ var Micra = (() => {
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 in target) return target[key];
528
- if (key in instance) return instance[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 (isRendering) return;
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 @@ var Micra = (() => {
544
696
  }
545
697
  };
546
698
  instance.destroy = function() {
547
- var _a2;
548
- (_a2 = instance.__micraSubs) == null ? void 0 : _a2.forEach((unsub) => unsub());
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);