micra.js 2.0.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/dist/micra.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v2.0.0 — https://github.com/micra-js/micra — MIT */
1
+ /* Micra.js v2.2.0 — https://github.com/micra-js/micra — MIT */
2
2
 
3
3
  // src/utils/fetch.ts
4
4
  function getCSRF() {
@@ -27,7 +27,8 @@ async function micraFetch(url, options = {}) {
27
27
  if (method === "GET" || method === "HEAD") {
28
28
  const params = {};
29
29
  for (const [k, v] of Object.entries(options)) {
30
- if (k !== "method" && k !== "headers" && v != null) params[k] = String(v);
30
+ if (k !== "method" && k !== "headers" && k !== "signal" && v != null)
31
+ params[k] = String(v);
31
32
  }
32
33
  if (Object.keys(params).length)
33
34
  finalUrl += (url.includes("?") ? "&" : "?") + new URLSearchParams(params);
@@ -38,6 +39,7 @@ async function micraFetch(url, options = {}) {
38
39
  const res = await fetch(finalUrl, {
39
40
  method,
40
41
  headers,
42
+ ...options.signal !== void 0 ? { signal: options.signal } : {},
41
43
  ...body !== void 0 ? { body } : {}
42
44
  });
43
45
  if (!res.ok)
@@ -219,27 +221,6 @@ function createScheduler(render) {
219
221
  };
220
222
  }
221
223
 
222
- // src/dom/query.ts
223
- function queryAll(root, sel) {
224
- return Array.from(root.querySelectorAll(sel));
225
- }
226
- function queryOwn(root, attr) {
227
- return filterOwn(root, queryAll(root, `[${attr}]`));
228
- }
229
- function queryOwnAll(root, sel) {
230
- return filterOwn(root, queryAll(root, sel));
231
- }
232
- function filterOwn(root, els) {
233
- return els.filter((el) => {
234
- let node = el.parentElement;
235
- while (node && node !== root) {
236
- if (node.hasAttribute("data-component")) return false;
237
- node = node.parentElement;
238
- }
239
- return true;
240
- });
241
- }
242
-
243
224
  // src/dom/directives.ts
244
225
  function applyText(el, expr, state) {
245
226
  var _a;
@@ -293,92 +274,37 @@ function applyClass(el, pairs, state) {
293
274
  el.classList.toggle(cls, Boolean(evalExpr(valExpr, state)));
294
275
  }
295
276
  }
296
- function parsePairs(expr) {
297
- const out = [];
298
- for (const part of expr.split(",")) {
299
- const colonIdx = part.indexOf(":");
300
- if (colonIdx === -1) continue;
301
- const left = part.slice(0, colonIdx).trim();
302
- const right = part.slice(colonIdx + 1).trim();
303
- if (!left) continue;
304
- out.push([left, right]);
305
- }
306
- return out;
307
- }
308
277
  function applyModel(el, key, rawState) {
309
278
  const html = el;
310
279
  const stateVal = rawState[key];
311
280
  const desired = stateVal == null ? "" : String(stateVal);
312
281
  if (html.value !== desired) html.value = desired;
313
282
  }
314
- function buildCache(root) {
315
- const pick = (attr) => {
316
- var _a;
317
- const els = queryOwn(root, attr);
318
- if ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, attr)) els.unshift(root);
319
- return els.filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
320
- };
321
- const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
322
- return {
323
- text: pick("data-text"),
324
- html: pick("data-html"),
325
- if: pick("data-if"),
326
- show: pick("data-show"),
327
- bind: pickPairs("data-bind"),
328
- model: pick("data-model"),
329
- class: pickPairs("data-class")
330
- };
331
- }
332
- function applyDirectives(root, state, rawState, _instance) {
333
- if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
334
- applyFromList(buildFragmentList(root), state, rawState);
335
- return;
336
- }
337
- const el = root;
338
- if (!el.__micraCache) el.__micraCache = buildCache(el);
339
- applyFromList(el.__micraCache, state, rawState);
340
- }
341
- function applyFromList(cache, state, rawState) {
342
- cache.if.forEach((b) => applyIf(b, state));
343
- cache.text.forEach((b) => applyText(b.el, b.expr, state));
344
- cache.html.forEach((b) => applyHtml(b.el, b.expr, state));
345
- cache.show.forEach((b) => applyShow(b.el, b.expr, state));
346
- cache.bind.forEach((b) => applyBind(b.el, b.pairs, state));
347
- cache.model.forEach((b) => applyModel(b.el, b.expr.trim(), rawState));
348
- cache.class.forEach((b) => applyClass(b.el, b.pairs, state));
349
- }
350
- function buildFragmentList(frag) {
351
- const pick = (attr) => queryAll(frag, `[${attr}]`).filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
352
- const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
353
- return {
354
- text: pick("data-text"),
355
- html: pick("data-html"),
356
- if: pick("data-if"),
357
- show: pick("data-show"),
358
- bind: pickPairs("data-bind"),
359
- model: pick("data-model"),
360
- class: pickPairs("data-class")
361
- };
362
- }
363
- function validateDirectives(root) {
364
- var _a, _b;
365
- queryOwn(root, "data-each").forEach((el) => {
283
+ function applyDirectives(scan, state, rawState, _instance) {
284
+ for (const b of scan.if) applyIf(b, state);
285
+ for (const b of scan.text) applyText(b.el, b.expr, state);
286
+ for (const b of scan.html) applyHtml(b.el, b.expr, state);
287
+ for (const b of scan.show) applyShow(b.el, b.expr, state);
288
+ for (const b of scan.bind) applyBind(b.el, b.pairs, state);
289
+ for (const b of scan.model) applyModel(b.el, b.expr.trim(), rawState);
290
+ for (const b of scan.class) applyClass(b.el, b.pairs, state);
291
+ }
292
+ function validateDirectives(scan) {
293
+ for (const el of scan.each) {
366
294
  const tmpl = el;
367
295
  if (!el.hasAttribute("data-key") && !tmpl.__micraNoKeyWarned) {
368
296
  tmpl.__micraNoKeyWarned = true;
369
- warn(`data-each="${el.getAttribute("data-each")}" has no data-key \u2014 keyed diff disabled. Add data-key="id" for better performance.`);
297
+ warn(
298
+ `data-each="${el.getAttribute("data-each")}" has no data-key \u2014 keyed diff disabled. Add data-key="id" for better performance.`
299
+ );
370
300
  }
371
- });
372
- const bindEls = queryOwn(root, "data-bind");
373
- if (((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-bind")) && !bindEls.includes(root)) bindEls.unshift(root);
374
- for (const el of bindEls) {
375
- const spec = (_b = el.getAttribute("data-bind")) != null ? _b : "";
376
- const hasClassBind = spec.split(",").some((p) => {
377
- var _a2;
378
- return ((_a2 = p.trim().split(":")[0]) == null ? void 0 : _a2.trim()) === "class";
379
- });
380
- if (hasClassBind && el.hasAttribute("data-class")) {
381
- warn(`element has both data-bind="class:..." and data-class \u2014 they fight on every render. Use one.`);
301
+ }
302
+ for (const b of scan.bind) {
303
+ const hasClassBind = b.pairs.some((p) => p[0] === "class");
304
+ if (hasClassBind && b.el.hasAttribute("data-class")) {
305
+ warn(
306
+ `element has both data-bind="class:..." and data-class \u2014 they fight on every render. Use one.`
307
+ );
382
308
  }
383
309
  }
384
310
  }
@@ -389,17 +315,13 @@ function track(instance, el, type, fn) {
389
315
  el.addEventListener(type, fn);
390
316
  ((_a = instance.__micraListeners) != null ? _a : instance.__micraListeners = []).push({ el, type, fn });
391
317
  }
392
- function bindDataOn(root, instance) {
393
- var _a, _b;
394
- const isFragment = root.nodeType === 11;
395
- const els = isFragment ? queryAll(root, "[data-on]") : queryOwn(root, "data-on");
396
- if (!isFragment && ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-on")) && !els.includes(root))
397
- els.unshift(root);
318
+ function bindDataOn(els, instance) {
319
+ var _a;
398
320
  for (const el of els) {
399
321
  const mEl = el;
400
322
  if (mEl.__micraEvents) continue;
401
323
  mEl.__micraEvents = true;
402
- const spec = (_b = mEl.dataset["on"]) != null ? _b : "";
324
+ const spec = (_a = mEl.dataset["on"]) != null ? _a : "";
403
325
  for (const part of spec.split(",")) {
404
326
  const [evSpec, method] = part.trim().split(":");
405
327
  if (!evSpec || !method) continue;
@@ -415,11 +337,8 @@ function bindDataOn(root, instance) {
415
337
  }
416
338
  }
417
339
  }
418
- function bindAtEvents(root, instance) {
419
- const isFragment = root.nodeType === 11;
420
- const all = isFragment ? queryAll(root, "*") : queryOwnAll(root, "*");
421
- if (!isFragment && !all.includes(root)) all.unshift(root);
422
- for (const el of all) {
340
+ function bindAtEvents(els, instance) {
341
+ for (const el of els) {
423
342
  const mEl = el;
424
343
  if (mEl.__micraAtBound) continue;
425
344
  let bound = false;
@@ -440,15 +359,12 @@ function bindAtEvents(root, instance) {
440
359
  if (bound) mEl.__micraAtBound = true;
441
360
  }
442
361
  }
443
- function bindModels(root, instance) {
444
- var _a;
445
- const isFragment = root.nodeType === 11;
446
- const els = isFragment ? queryAll(root, "[data-model]") : queryOwn(root, "data-model");
447
- for (const el of els) {
362
+ function bindModels(bindings, instance) {
363
+ for (const { el, expr } of bindings) {
448
364
  const mEl = el;
449
365
  if (mEl.__micraModel) continue;
450
366
  mEl.__micraModel = true;
451
- const key = (_a = el.dataset["model"]) != null ? _a : "";
367
+ const key = expr.trim();
452
368
  const tag = el.tagName;
453
369
  const inputEl = el;
454
370
  const inputType = inputEl.type;
@@ -469,11 +385,132 @@ function bindModels(root, instance) {
469
385
  }
470
386
  }
471
387
 
388
+ // src/dom/scan.ts
389
+ function emptyScan() {
390
+ return {
391
+ text: [],
392
+ html: [],
393
+ if: [],
394
+ show: [],
395
+ bind: [],
396
+ model: [],
397
+ class: [],
398
+ each: [],
399
+ on: [],
400
+ atEvents: [],
401
+ refs: []
402
+ };
403
+ }
404
+ function parsePairs(expr) {
405
+ const out = [];
406
+ for (const part of expr.split(",")) {
407
+ const colon = part.indexOf(":");
408
+ if (colon === -1) continue;
409
+ const left = part.slice(0, colon).trim();
410
+ const right = part.slice(colon + 1).trim();
411
+ if (!left) continue;
412
+ out.push([left, right]);
413
+ }
414
+ return out;
415
+ }
416
+ function classify(el, scan) {
417
+ if (el.tagName === "TEMPLATE") {
418
+ if (el.hasAttribute("data-each")) scan.each.push(el);
419
+ return;
420
+ }
421
+ const attrs = el.attributes;
422
+ let atEventSeen = false;
423
+ for (let i = 0; i < attrs.length; i++) {
424
+ const a = attrs[i];
425
+ const name = a.name;
426
+ const first = name.charCodeAt(0);
427
+ if (first === 64) {
428
+ if (!atEventSeen) {
429
+ scan.atEvents.push(el);
430
+ atEventSeen = true;
431
+ }
432
+ continue;
433
+ }
434
+ if (first === 100 && name.length >= 6 && name.charCodeAt(4) === 45) {
435
+ const rest = name.slice(5);
436
+ switch (rest) {
437
+ case "text":
438
+ scan.text.push({ el, expr: a.value });
439
+ break;
440
+ case "html":
441
+ scan.html.push({ el, expr: a.value });
442
+ break;
443
+ case "if":
444
+ scan.if.push({ el, expr: a.value });
445
+ break;
446
+ case "show":
447
+ scan.show.push({ el, expr: a.value });
448
+ break;
449
+ case "bind": {
450
+ const pairs = parsePairs(a.value);
451
+ scan.bind.push({ el, expr: a.value, pairs });
452
+ break;
453
+ }
454
+ case "model":
455
+ scan.model.push({ el, expr: a.value });
456
+ break;
457
+ case "class": {
458
+ const pairs = parsePairs(a.value);
459
+ scan.class.push({ el, expr: a.value, pairs });
460
+ break;
461
+ }
462
+ case "on":
463
+ scan.on.push(el);
464
+ break;
465
+ case "ref":
466
+ scan.refs.push(el);
467
+ break;
468
+ }
469
+ }
470
+ }
471
+ }
472
+ var NESTED_COMPONENT_FILTER = {
473
+ acceptNode(node) {
474
+ if (node.hasAttribute("data-component"))
475
+ return NodeFilter.FILTER_REJECT;
476
+ return NodeFilter.FILTER_ACCEPT;
477
+ }
478
+ };
479
+ function scanComponent(root) {
480
+ const scan = emptyScan();
481
+ classify(root, scan);
482
+ const walker = document.createTreeWalker(
483
+ root,
484
+ NodeFilter.SHOW_ELEMENT,
485
+ NESTED_COMPONENT_FILTER
486
+ );
487
+ let node = walker.nextNode();
488
+ while (node) {
489
+ classify(node, scan);
490
+ node = walker.nextNode();
491
+ }
492
+ return scan;
493
+ }
494
+ function scanFragment(frag) {
495
+ const scan = emptyScan();
496
+ const walker = document.createTreeWalker(
497
+ frag,
498
+ NodeFilter.SHOW_ELEMENT,
499
+ NESTED_COMPONENT_FILTER
500
+ );
501
+ let node = walker.nextNode();
502
+ while (node) {
503
+ classify(node, scan);
504
+ node = walker.nextNode();
505
+ }
506
+ return scan;
507
+ }
508
+
472
509
  // src/dom/each.ts
473
- function renderList(root, state, rawState, instance) {
474
- queryOwn(root, "data-each").forEach((tmplEl) => {
475
- var _a;
476
- if (tmplEl.tagName !== "TEMPLATE") return;
510
+ function renderList(templates, state, rawState, instance) {
511
+ var _a;
512
+ for (const tmplEl of templates) {
513
+ if (tmplEl.tagName !== "TEMPLATE") continue;
477
514
  const tmpl = tmplEl;
478
515
  const itemsExpr = tmpl.getAttribute("data-each");
479
516
  const keyAttr = (_a = tmpl.getAttribute("data-key")) != null ? _a : null;
@@ -488,21 +525,22 @@ function renderList(root, state, rawState, instance) {
488
525
  const marker = tmpl.__micraMarker;
489
526
  const keyMap = tmpl.__micraNodes;
490
527
  const parent = marker.parentNode;
491
- if (!parent) return;
528
+ if (!parent) continue;
492
529
  if (!Array.isArray(items)) {
493
530
  tmpl.__micraList.forEach((n) => n.remove());
494
531
  tmpl.__micraList = [];
495
532
  keyMap.clear();
496
- return;
533
+ continue;
497
534
  }
498
535
  if (keyAttr) {
499
536
  renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance);
500
537
  } else {
501
538
  renderNoKey(tmpl, items, marker, parent, state, rawState, instance);
502
539
  }
503
- });
540
+ }
504
541
  }
505
542
  function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
543
+ var _a;
506
544
  const nextKeys = /* @__PURE__ */ new Set();
507
545
  const nextNodes = [];
508
546
  let warnedNullKey = false;
@@ -530,14 +568,18 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
530
568
  }
531
569
  node.__micraKey = key;
532
570
  keyMap.set(key, node);
533
- bindDataOn(node, instance);
534
- bindAtEvents(node, instance);
571
+ const rowScan2 = scanComponent(node);
572
+ node.__micraScan = rowScan2;
573
+ bindDataOn(rowScan2.on, instance);
574
+ bindAtEvents(rowScan2.atEvents, instance);
575
+ bindModels(rowScan2.model, instance);
535
576
  }
536
577
  const itemState = Object.assign(
537
578
  Object.create(state),
538
579
  { item, index, $index: index }
539
580
  );
540
- applyDirectives(node, itemState, rawState, instance);
581
+ const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
582
+ applyDirectives(rowScan, itemState, rawState, instance);
541
583
  nextNodes.push(node);
542
584
  }
543
585
  for (const [key, node] of keyMap) {
@@ -563,9 +605,11 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
563
605
  Object.create(state),
564
606
  { item, index, $index: index }
565
607
  );
566
- applyDirectives(clone, itemState, rawState, instance);
567
- bindDataOn(clone, instance);
568
- bindAtEvents(clone, instance);
608
+ const fragScan = scanFragment(clone);
609
+ applyDirectives(fragScan, itemState, rawState, instance);
610
+ bindDataOn(fragScan.on, instance);
611
+ bindAtEvents(fragScan.atEvents, instance);
612
+ bindModels(fragScan.model, instance);
569
613
  const nodes = Array.from(clone.childNodes);
570
614
  nodes.forEach((n) => {
571
615
  n.__micraEach = true;
@@ -577,9 +621,9 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
577
621
  }
578
622
 
579
623
  // src/dom/refs.ts
580
- function collectRefs(root, instance) {
624
+ function collectRefs(els, instance) {
581
625
  instance.refs = {};
582
- for (const el of queryOwn(root, "data-ref")) {
626
+ for (const el of els) {
583
627
  const name = el.dataset["ref"];
584
628
  if (name) instance.refs[name] = el;
585
629
  }
@@ -593,10 +637,13 @@ function mount(selector, definition) {
593
637
  warn(`"${selector}" not found`);
594
638
  return null;
595
639
  }
596
- if (_instances.has(root)) return _instances.get(root);
640
+ if (_instances.has(root))
641
+ return _instances.get(root);
597
642
  const rawState = { ...(_a = definition.state) != null ? _a : {} };
598
643
  const instance = { $el: root, refs: {} };
599
- for (const [key, val] of Object.entries(definition)) {
644
+ for (const [key, val] of Object.entries(
645
+ definition
646
+ )) {
600
647
  if (key === "state" || key === "onCreate" || key === "onDestroy") continue;
601
648
  if (typeof val === "function") instance[key] = val;
602
649
  }
@@ -640,22 +687,27 @@ function mount(selector, definition) {
640
687
  });
641
688
  let warnedReentry = false;
642
689
  instance.render = function() {
690
+ var _a2;
643
691
  if (instance.__micraDestroyed) return;
644
692
  if (isRendering) {
645
693
  if (!warnedReentry) {
646
- warn("render() re-entry detected \u2014 mutation inside a directive expression is ignored. Move state writes to a method.");
694
+ warn(
695
+ "render() re-entry detected \u2014 mutation inside a directive expression is ignored. Move state writes to a method."
696
+ );
647
697
  warnedReentry = true;
648
698
  }
649
699
  return;
650
700
  }
651
701
  isRendering = true;
652
702
  try {
653
- applyDirectives(root, exprState, rawState, instance);
654
- renderList(root, exprState, rawState, instance);
655
- bindDataOn(root, instance);
656
- bindAtEvents(root, instance);
657
- bindModels(root, instance);
658
- collectRefs(root, instance);
703
+ const mRoot2 = root;
704
+ const scan = (_a2 = mRoot2.__micraScan) != null ? _a2 : mRoot2.__micraScan = scanComponent(root);
705
+ applyDirectives(scan, exprState, rawState, instance);
706
+ renderList(scan.each, exprState, rawState, instance);
707
+ bindDataOn(scan.on, instance);
708
+ bindAtEvents(scan.atEvents, instance);
709
+ bindModels(scan.model, instance);
710
+ collectRefs(scan.refs, instance);
659
711
  } finally {
660
712
  isRendering = false;
661
713
  }
@@ -664,14 +716,16 @@ function mount(selector, definition) {
664
716
  var _a2, _b;
665
717
  if (instance.__micraDestroyed) return;
666
718
  instance.__micraDestroyed = true;
667
- (_a2 = instance.__micraListeners) == null ? void 0 : _a2.forEach(({ el, type, fn }) => el.removeEventListener(type, fn));
719
+ (_a2 = instance.__micraListeners) == null ? void 0 : _a2.forEach(
720
+ ({ el, type, fn }) => el.removeEventListener(type, fn)
721
+ );
668
722
  instance.__micraListeners = [];
669
723
  const clearFlags = (el) => {
670
724
  const m = el;
671
725
  delete m.__micraEvents;
672
726
  delete m.__micraAtBound;
673
727
  delete m.__micraModel;
674
- delete m.__micraCache;
728
+ delete m.__micraScan;
675
729
  };
676
730
  clearFlags(root);
677
731
  root.querySelectorAll("*").forEach(clearFlags);
@@ -683,7 +737,8 @@ function mount(selector, definition) {
683
737
  };
684
738
  _instances.set(root, instance);
685
739
  instance.render();
686
- validateDirectives(root);
740
+ const mRoot = root;
741
+ if (mRoot.__micraScan) validateDirectives(mRoot.__micraScan);
687
742
  if (typeof definition.onCreate === "function")
688
743
  Promise.resolve().then(
689
744
  () => definition.onCreate.call(instance)