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/dist/micra.esm.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Micra.js v2.1.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() {
@@ -221,27 +221,6 @@ function createScheduler(render) {
221
221
  };
222
222
  }
223
223
 
224
- // src/dom/query.ts
225
- function queryAll(root, sel) {
226
- return Array.from(root.querySelectorAll(sel));
227
- }
228
- function queryOwn(root, attr) {
229
- return filterOwn(root, queryAll(root, `[${attr}]`));
230
- }
231
- function queryOwnAll(root, sel) {
232
- return filterOwn(root, queryAll(root, sel));
233
- }
234
- function filterOwn(root, els) {
235
- return els.filter((el) => {
236
- let node = el.parentElement;
237
- while (node && node !== root) {
238
- if (node.hasAttribute("data-component")) return false;
239
- node = node.parentElement;
240
- }
241
- return true;
242
- });
243
- }
244
-
245
224
  // src/dom/directives.ts
246
225
  function applyText(el, expr, state) {
247
226
  var _a;
@@ -295,92 +274,37 @@ function applyClass(el, pairs, state) {
295
274
  el.classList.toggle(cls, Boolean(evalExpr(valExpr, state)));
296
275
  }
297
276
  }
298
- function parsePairs(expr) {
299
- const out = [];
300
- for (const part of expr.split(",")) {
301
- const colonIdx = part.indexOf(":");
302
- if (colonIdx === -1) continue;
303
- const left = part.slice(0, colonIdx).trim();
304
- const right = part.slice(colonIdx + 1).trim();
305
- if (!left) continue;
306
- out.push([left, right]);
307
- }
308
- return out;
309
- }
310
277
  function applyModel(el, key, rawState) {
311
278
  const html = el;
312
279
  const stateVal = rawState[key];
313
280
  const desired = stateVal == null ? "" : String(stateVal);
314
281
  if (html.value !== desired) html.value = desired;
315
282
  }
316
- function buildCache(root) {
317
- const pick = (attr) => {
318
- var _a;
319
- const els = queryOwn(root, attr);
320
- if ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, attr)) els.unshift(root);
321
- return els.filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
322
- };
323
- const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
324
- return {
325
- text: pick("data-text"),
326
- html: pick("data-html"),
327
- if: pick("data-if"),
328
- show: pick("data-show"),
329
- bind: pickPairs("data-bind"),
330
- model: pick("data-model"),
331
- class: pickPairs("data-class")
332
- };
333
- }
334
- function applyDirectives(root, state, rawState, _instance) {
335
- if (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
336
- applyFromList(buildFragmentList(root), state, rawState);
337
- return;
338
- }
339
- const el = root;
340
- if (!el.__micraCache) el.__micraCache = buildCache(el);
341
- applyFromList(el.__micraCache, state, rawState);
342
- }
343
- function applyFromList(cache, state, rawState) {
344
- cache.if.forEach((b) => applyIf(b, state));
345
- cache.text.forEach((b) => applyText(b.el, b.expr, state));
346
- cache.html.forEach((b) => applyHtml(b.el, b.expr, state));
347
- cache.show.forEach((b) => applyShow(b.el, b.expr, state));
348
- cache.bind.forEach((b) => applyBind(b.el, b.pairs, state));
349
- cache.model.forEach((b) => applyModel(b.el, b.expr.trim(), rawState));
350
- cache.class.forEach((b) => applyClass(b.el, b.pairs, state));
351
- }
352
- function buildFragmentList(frag) {
353
- const pick = (attr) => queryAll(frag, `[${attr}]`).filter((el) => !el.closest("template")).map((el) => ({ el, expr: el.getAttribute(attr) }));
354
- const pickPairs = (attr) => pick(attr).map((b) => ({ ...b, pairs: parsePairs(b.expr) }));
355
- return {
356
- text: pick("data-text"),
357
- html: pick("data-html"),
358
- if: pick("data-if"),
359
- show: pick("data-show"),
360
- bind: pickPairs("data-bind"),
361
- model: pick("data-model"),
362
- class: pickPairs("data-class")
363
- };
364
- }
365
- function validateDirectives(root) {
366
- var _a, _b;
367
- 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) {
368
294
  const tmpl = el;
369
295
  if (!el.hasAttribute("data-key") && !tmpl.__micraNoKeyWarned) {
370
296
  tmpl.__micraNoKeyWarned = true;
371
- 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
+ );
372
300
  }
373
- });
374
- const bindEls = queryOwn(root, "data-bind");
375
- if (((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-bind")) && !bindEls.includes(root)) bindEls.unshift(root);
376
- for (const el of bindEls) {
377
- const spec = (_b = el.getAttribute("data-bind")) != null ? _b : "";
378
- const hasClassBind = spec.split(",").some((p) => {
379
- var _a2;
380
- return ((_a2 = p.trim().split(":")[0]) == null ? void 0 : _a2.trim()) === "class";
381
- });
382
- if (hasClassBind && el.hasAttribute("data-class")) {
383
- 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
+ );
384
308
  }
385
309
  }
386
310
  }
@@ -391,17 +315,13 @@ function track(instance, el, type, fn) {
391
315
  el.addEventListener(type, fn);
392
316
  ((_a = instance.__micraListeners) != null ? _a : instance.__micraListeners = []).push({ el, type, fn });
393
317
  }
394
- function bindDataOn(root, instance) {
395
- var _a, _b;
396
- const isFragment = root.nodeType === 11;
397
- const els = isFragment ? queryAll(root, "[data-on]") : queryOwn(root, "data-on");
398
- if (!isFragment && ((_a = root.hasAttribute) == null ? void 0 : _a.call(root, "data-on")) && !els.includes(root))
399
- els.unshift(root);
318
+ function bindDataOn(els, instance) {
319
+ var _a;
400
320
  for (const el of els) {
401
321
  const mEl = el;
402
322
  if (mEl.__micraEvents) continue;
403
323
  mEl.__micraEvents = true;
404
- const spec = (_b = mEl.dataset["on"]) != null ? _b : "";
324
+ const spec = (_a = mEl.dataset["on"]) != null ? _a : "";
405
325
  for (const part of spec.split(",")) {
406
326
  const [evSpec, method] = part.trim().split(":");
407
327
  if (!evSpec || !method) continue;
@@ -417,11 +337,8 @@ function bindDataOn(root, instance) {
417
337
  }
418
338
  }
419
339
  }
420
- function bindAtEvents(root, instance) {
421
- const isFragment = root.nodeType === 11;
422
- const all = isFragment ? queryAll(root, "*") : queryOwnAll(root, "*");
423
- if (!isFragment && !all.includes(root)) all.unshift(root);
424
- for (const el of all) {
340
+ function bindAtEvents(els, instance) {
341
+ for (const el of els) {
425
342
  const mEl = el;
426
343
  if (mEl.__micraAtBound) continue;
427
344
  let bound = false;
@@ -442,15 +359,12 @@ function bindAtEvents(root, instance) {
442
359
  if (bound) mEl.__micraAtBound = true;
443
360
  }
444
361
  }
445
- function bindModels(root, instance) {
446
- var _a;
447
- const isFragment = root.nodeType === 11;
448
- const els = isFragment ? queryAll(root, "[data-model]") : queryOwn(root, "data-model");
449
- for (const el of els) {
362
+ function bindModels(bindings, instance) {
363
+ for (const { el, expr } of bindings) {
450
364
  const mEl = el;
451
365
  if (mEl.__micraModel) continue;
452
366
  mEl.__micraModel = true;
453
- const key = (_a = el.dataset["model"]) != null ? _a : "";
367
+ const key = expr.trim();
454
368
  const tag = el.tagName;
455
369
  const inputEl = el;
456
370
  const inputType = inputEl.type;
@@ -471,11 +385,132 @@ function bindModels(root, instance) {
471
385
  }
472
386
  }
473
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
+
474
509
  // src/dom/each.ts
475
- function renderList(root, state, rawState, instance) {
476
- queryOwn(root, "data-each").forEach((tmplEl) => {
477
- var _a;
478
- 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;
479
514
  const tmpl = tmplEl;
480
515
  const itemsExpr = tmpl.getAttribute("data-each");
481
516
  const keyAttr = (_a = tmpl.getAttribute("data-key")) != null ? _a : null;
@@ -490,21 +525,22 @@ function renderList(root, state, rawState, instance) {
490
525
  const marker = tmpl.__micraMarker;
491
526
  const keyMap = tmpl.__micraNodes;
492
527
  const parent = marker.parentNode;
493
- if (!parent) return;
528
+ if (!parent) continue;
494
529
  if (!Array.isArray(items)) {
495
530
  tmpl.__micraList.forEach((n) => n.remove());
496
531
  tmpl.__micraList = [];
497
532
  keyMap.clear();
498
- return;
533
+ continue;
499
534
  }
500
535
  if (keyAttr) {
501
536
  renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance);
502
537
  } else {
503
538
  renderNoKey(tmpl, items, marker, parent, state, rawState, instance);
504
539
  }
505
- });
540
+ }
506
541
  }
507
542
  function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawState, instance) {
543
+ var _a;
508
544
  const nextKeys = /* @__PURE__ */ new Set();
509
545
  const nextNodes = [];
510
546
  let warnedNullKey = false;
@@ -532,14 +568,18 @@ function renderKeyed(tmpl, items, keyAttr, marker, keyMap, parent, state, rawSta
532
568
  }
533
569
  node.__micraKey = key;
534
570
  keyMap.set(key, node);
535
- bindDataOn(node, instance);
536
- 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);
537
576
  }
538
577
  const itemState = Object.assign(
539
578
  Object.create(state),
540
579
  { item, index, $index: index }
541
580
  );
542
- applyDirectives(node, itemState, rawState, instance);
581
+ const rowScan = (_a = node.__micraScan) != null ? _a : node.__micraScan = scanComponent(node);
582
+ applyDirectives(rowScan, itemState, rawState, instance);
543
583
  nextNodes.push(node);
544
584
  }
545
585
  for (const [key, node] of keyMap) {
@@ -565,9 +605,11 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
565
605
  Object.create(state),
566
606
  { item, index, $index: index }
567
607
  );
568
- applyDirectives(clone, itemState, rawState, instance);
569
- bindDataOn(clone, instance);
570
- 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);
571
613
  const nodes = Array.from(clone.childNodes);
572
614
  nodes.forEach((n) => {
573
615
  n.__micraEach = true;
@@ -579,9 +621,9 @@ function renderNoKey(tmpl, items, marker, parent, state, rawState, instance) {
579
621
  }
580
622
 
581
623
  // src/dom/refs.ts
582
- function collectRefs(root, instance) {
624
+ function collectRefs(els, instance) {
583
625
  instance.refs = {};
584
- for (const el of queryOwn(root, "data-ref")) {
626
+ for (const el of els) {
585
627
  const name = el.dataset["ref"];
586
628
  if (name) instance.refs[name] = el;
587
629
  }
@@ -595,10 +637,13 @@ function mount(selector, definition) {
595
637
  warn(`"${selector}" not found`);
596
638
  return null;
597
639
  }
598
- if (_instances.has(root)) return _instances.get(root);
640
+ if (_instances.has(root))
641
+ return _instances.get(root);
599
642
  const rawState = { ...(_a = definition.state) != null ? _a : {} };
600
643
  const instance = { $el: root, refs: {} };
601
- for (const [key, val] of Object.entries(definition)) {
644
+ for (const [key, val] of Object.entries(
645
+ definition
646
+ )) {
602
647
  if (key === "state" || key === "onCreate" || key === "onDestroy") continue;
603
648
  if (typeof val === "function") instance[key] = val;
604
649
  }
@@ -642,22 +687,27 @@ function mount(selector, definition) {
642
687
  });
643
688
  let warnedReentry = false;
644
689
  instance.render = function() {
690
+ var _a2;
645
691
  if (instance.__micraDestroyed) return;
646
692
  if (isRendering) {
647
693
  if (!warnedReentry) {
648
- 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
+ );
649
697
  warnedReentry = true;
650
698
  }
651
699
  return;
652
700
  }
653
701
  isRendering = true;
654
702
  try {
655
- applyDirectives(root, exprState, rawState, instance);
656
- renderList(root, exprState, rawState, instance);
657
- bindDataOn(root, instance);
658
- bindAtEvents(root, instance);
659
- bindModels(root, instance);
660
- 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);
661
711
  } finally {
662
712
  isRendering = false;
663
713
  }
@@ -666,14 +716,16 @@ function mount(selector, definition) {
666
716
  var _a2, _b;
667
717
  if (instance.__micraDestroyed) return;
668
718
  instance.__micraDestroyed = true;
669
- (_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
+ );
670
722
  instance.__micraListeners = [];
671
723
  const clearFlags = (el) => {
672
724
  const m = el;
673
725
  delete m.__micraEvents;
674
726
  delete m.__micraAtBound;
675
727
  delete m.__micraModel;
676
- delete m.__micraCache;
728
+ delete m.__micraScan;
677
729
  };
678
730
  clearFlags(root);
679
731
  root.querySelectorAll("*").forEach(clearFlags);
@@ -685,7 +737,8 @@ function mount(selector, definition) {
685
737
  };
686
738
  _instances.set(root, instance);
687
739
  instance.render();
688
- validateDirectives(root);
740
+ const mRoot = root;
741
+ if (mRoot.__micraScan) validateDirectives(mRoot.__micraScan);
689
742
  if (typeof definition.onCreate === "function")
690
743
  Promise.resolve().then(
691
744
  () => definition.onCreate.call(instance)