solarite 0.1.0 → 0.1.1

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.
@@ -81,16 +81,6 @@ var Util$1 = {
81
81
 
82
82
  };
83
83
 
84
- //#IFDEV
85
- /*@__NO_SIDE_EFFECTS__*/
86
- function assert(val) {
87
- if (!val) {
88
- debugger;
89
- throw new Error('Assertion failed: ' + val);
90
- }
91
- }
92
- //#ENDIF
93
-
94
84
  /**
95
85
  * Follow a path into an object.
96
86
  * @param obj {object}
@@ -188,19 +178,19 @@ function getArg(el, name, val=null, type=ArgType.String) {
188
178
  case ArgType.Float:
189
179
  return parseFloat(val);
190
180
  case ArgType.String:
191
- return val || '';
181
+ return [undefined, null, false].includes(val) ? '' : val+'';
192
182
  case ArgType.JSON:
193
- try {
194
- return JSON.parse(val);
195
- } catch (e) {
196
- return val;
197
- }
198
183
  case ArgType.Eval:
199
- try {
200
- return eval(`(${val})`);
201
- } catch (e) {
202
- return val;
203
- }
184
+ if (typeof val === 'string' && val.length)
185
+ try {
186
+ if (type === ArgType.JSON)
187
+ return JSON.parse(val);
188
+ else
189
+ return eval(`(${val})`);
190
+ } catch (e) {
191
+ return val;
192
+ }
193
+ else return val;
204
194
  default:
205
195
  return val;
206
196
  }
@@ -251,7 +241,7 @@ function getObjectId(obj, prefix=null) {
251
241
  prefix = 'Func';
252
242
  else if (typeof obj === 'object')
253
243
  prefix = 'Obj';
254
- }
244
+ }
255
245
  //#ENDIF
256
246
 
257
247
  prefix = prefix || '~\f';
@@ -354,7 +344,11 @@ class MultiValueMap {
354
344
  return this.data[key] || [];
355
345
  }
356
346
 
357
- // Remove one value from a key, and return it
347
+ /**
348
+ * Remove one value from a key, and return it.
349
+ * @param key {string}
350
+ * @param val If specified, make sure we delete this specific value, if a key exists more than once.
351
+ * @returns {*} */
358
352
  delete(key, val=undefined) {
359
353
  // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
360
354
  // debugger;
@@ -399,6 +393,16 @@ class MultiValueMap {
399
393
  }
400
394
  }
401
395
 
396
+ //#IFDEV
397
+ /*@__NO_SIDE_EFFECTS__*/
398
+ function assert(val) {
399
+ if (!val) {
400
+ debugger;
401
+ throw new Error('Assertion failed: ' + val);
402
+ }
403
+ }
404
+ //#ENDIF
405
+
402
406
  let Util = {
403
407
 
404
408
  bindStyles(style, root) {
@@ -619,6 +623,209 @@ function flattenAndIndent(inputArray, indent = "") {
619
623
  }
620
624
  //#ENDIF
621
625
 
626
+ /**
627
+ * The html strings and evaluated expressions from an html tagged template.
628
+ * A unique Template is created for each item in a loop.
629
+ * Although the reference to the html strings is shared among templates. */
630
+ class Template {
631
+
632
+ /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
633
+ exprs = []
634
+
635
+ /** @type {string[]} */
636
+ html = [];
637
+
638
+ /**
639
+ * If true, use this template to replace an existing element, instead of appending children to it.
640
+ * @type {?boolean} */
641
+ replaceMode;
642
+
643
+ /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
644
+ hashedFields;
645
+
646
+ /**
647
+ * @deprecated
648
+ * @type {ExprPath} Used with forEach() from watch.js
649
+ * Set in ExprPath.apply() */
650
+ parentPath;
651
+
652
+ /** @type {NodeGroup} */
653
+ nodeGroup;
654
+
655
+ /**
656
+ * @type {string[][]} */
657
+ paths = [];
658
+
659
+ /**
660
+ *
661
+ * @param htmlStrings {string[]}
662
+ * @param exprs {*[]} */
663
+ constructor(htmlStrings, exprs) {
664
+ this.html = htmlStrings;
665
+ this.exprs = exprs;
666
+
667
+ //this.trace = new Error().stack.split(/\n/g)
668
+
669
+ // Multiple templates can share the same htmlStrings array.
670
+ //this.hashedFields = [getObjectId(htmlStrings), exprs]
671
+
672
+ //#IFDEV
673
+ assert(Array.isArray(htmlStrings));
674
+ assert(Array.isArray(exprs));
675
+
676
+ Object.defineProperty(this, 'debug', {
677
+ get() {
678
+ return JSON.stringify([this.html, this.exprs]);
679
+ }
680
+ });
681
+ //#ENDIF
682
+ }
683
+
684
+ /**
685
+ * Called by JSON.serialize when it encounters a Template.
686
+ * This prevents the hashed version from being too large. */
687
+ toJSON() {
688
+ if (!this.hashedFields)
689
+ this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
690
+
691
+ return this.hashedFields
692
+ }
693
+
694
+ /**
695
+ * Render the main template, which may indirectly call renderTemplate() to create children.
696
+ * @param el {HTMLElement}
697
+ * @param options {RenderOptions}
698
+ * @return {?DocumentFragment|HTMLElement} */
699
+ render(el=null, options={}) {
700
+
701
+ let ng;
702
+ if (!el) {
703
+ ng = new NodeGroup(this);
704
+ el = ng.getParentNode();
705
+ }
706
+
707
+ let ngm = NodeGroupManager.get(el);
708
+ if (ng)
709
+ ng.manager = ngm;
710
+
711
+ //#IFDEV
712
+ ngm.modifications = {
713
+ created: [],
714
+ updated: [],
715
+ moved: [],
716
+ deleted: []
717
+ };
718
+ //#ENDIF
719
+
720
+ ngm.options = options;
721
+ ngm.clearSubscribers = false; // Used for deprecated watch() path?
722
+ ngm.mutationWatcherEnabled = false;
723
+
724
+ // Fast path for empty component.
725
+ if (this.html?.length === 1 && !this.html[0]) {
726
+ el.innerHTML = '';
727
+ }
728
+ else {
729
+
730
+ // Find or create a NodeGroup for the template.
731
+ // This updates all nodes from the template.
732
+ let close;
733
+ let exact = ngm.getNodeGroup(this, true);
734
+ if (!exact) {
735
+ close = ngm.getNodeGroup(this, false);
736
+ }
737
+
738
+ let firstTime = !ngm.rootNg;
739
+ ngm.rootNg = exact || close;
740
+
741
+ // Reparent NodeGroup
742
+ // TODO: Move this to NodeGroup?
743
+ let parent = ngm.rootNg.getParentNode();
744
+
745
+
746
+ // If this is the first time rendering this element.
747
+ if (firstTime) {
748
+
749
+ // Save slot children
750
+ let fragment;
751
+ if (el.childNodes.length) {
752
+ fragment = document.createDocumentFragment();
753
+ fragment.append(...el.childNodes);
754
+ }
755
+
756
+ // Add rendered elements.
757
+ if (parent instanceof DocumentFragment)
758
+ el.append(parent);
759
+ else if (parent)
760
+ el.append(...parent.childNodes);
761
+
762
+ // Apply slot children
763
+ if (fragment) {
764
+ for (let slot of el.querySelectorAll('slot[name]')) {
765
+ let name = slot.getAttribute('name');
766
+ if (name)
767
+ slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
768
+ }
769
+ let unamedSlot = el.querySelector('slot:not([name])');
770
+ if (unamedSlot)
771
+ unamedSlot.append(fragment);
772
+ }
773
+ }
774
+
775
+ ngm.rootEl = el;
776
+
777
+ // this.rootNg was rendered as childrenOnly=true
778
+ // Apply attributes from a root element to the real root element.
779
+ let ng = ngm.rootNg;
780
+ if (ng.pseudoRoot && ng.pseudoRoot !== el) {
781
+ /*#IFDEV*/assert(el);/*#ENDIF*/
782
+
783
+ // Remove old attributes
784
+ // for (let attrib of this.rootEl.attributes)
785
+ // if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
786
+ // this.rootEl.removeAttribute(attrib.name)
787
+
788
+ // Add/set new attributes
789
+ if (firstTime)
790
+ for (let attrib of ng.pseudoRoot.attributes)
791
+ if (!el.hasAttribute(attrib.name))
792
+ el.setAttribute(attrib.name, attrib.value);
793
+
794
+ // ng.startNode = ng.endNode = this.rootEl;
795
+ // ng.nodesCache = [ng.startNode]
796
+ // for (let path of ng.paths) {
797
+ // if (path.nodeMarker === ng.rootEl)
798
+ // path.nodeMarker = this.rootEl;
799
+ // path.nodesCache = null;
800
+ // /*#IFDEV*/assert(path.nodeBefore !== ng.rootEl)/*#ENDIF*/
801
+ // }
802
+ //
803
+ // ng.rootEl = this.rootEl;
804
+ }
805
+
806
+ /*#IFDEV*/ngm.rootNg.verify();/*#ENDIF*/
807
+ ngm.reset(); // Mark all NodeGroups as available, for next render.
808
+ /*#IFDEV*/ngm.rootNg.verify();/*#ENDIF*/
809
+
810
+ window.ngm = ngm;
811
+ }
812
+
813
+ ngm.mutationWatcherEnabled = true;
814
+ return el;
815
+ //#IFDEV
816
+ //return ngm.modifications;
817
+ //#ENDIF
818
+ }
819
+
820
+
821
+ getCloseKey() {
822
+ // Use the joined html when debugging?
823
+ //return '@'+this.html.join('|')
824
+
825
+ return '@'+this.hashedFields[0];
826
+ }
827
+ }
828
+
622
829
  /**
623
830
  * Path to where an expression should be evaluated within a Shell.
624
831
  * Path is only valid until the expressions before it are evaluated.
@@ -737,6 +944,90 @@ class ExprPath {
737
944
  this.attrNames = new Set();
738
945
  }
739
946
 
947
+ /**
948
+ *
949
+ * @param expr {Template|Node|Array|function|*}
950
+ * @param newNodes {(Node|Template)[]}
951
+ * @param secondPass {Array} Locations within newNodes to evaluate later. */
952
+ apply(expr, newNodes, secondPass) {
953
+
954
+ if (expr instanceof Template) {
955
+ expr.nodegroup = this.parentNg; // All tests pass w/o this.
956
+
957
+ let ng = this.parentNg.manager.getNodeGroup(expr, true);
958
+
959
+
960
+ if (ng) {
961
+ //#IFDEV
962
+ // Make sure the nodeCache of the ExprPath we took it from is sitll valid.
963
+ if (ng.parentPath)
964
+ ng.parentPath.verify();
965
+ //#ENDIF
966
+
967
+
968
+ // TODO: Track ranges of changed nodes and only pass those to udomdiff?
969
+ // But will that break the swap benchmark?
970
+ newNodes.push(...ng.getNodes());
971
+ this.nodeGroups.push(ng);
972
+ }
973
+
974
+ // If expression, evaluate later to find partial match.
975
+ else {
976
+ secondPass.push([newNodes.length, this.nodeGroups.length]);
977
+ newNodes.push(expr);
978
+ this.nodeGroups.push(null); // placeholder
979
+ }
980
+ }
981
+
982
+ // Node created by an expression.
983
+ else if (expr instanceof Node) {
984
+
985
+ // DocumentFragment created by an expression.
986
+ if (expr instanceof DocumentFragment)
987
+ newNodes.push(...expr.childNodes);
988
+ else
989
+ newNodes.push(expr);
990
+ }
991
+
992
+ else if (Array.isArray(expr))
993
+ for (let subExpr of expr)
994
+ this.apply(subExpr, newNodes, secondPass);
995
+
996
+ else if (typeof expr === 'function') {
997
+ let result = expr();
998
+
999
+ this.apply(result, newNodes, secondPass);
1000
+ }
1001
+
1002
+ // Text
1003
+ else {
1004
+ // Convert falsy values (but not 0) to empty string.
1005
+ // Convert numbers to string so they compare the same.
1006
+ let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
1007
+
1008
+ // Fast path for updating the text of a single text node.
1009
+ let first = this.nodeBefore.nextSibling;
1010
+ if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
1011
+ if (first.textContent !== text)
1012
+ first.textContent = text;
1013
+
1014
+ newNodes.push(first);
1015
+ }
1016
+
1017
+ else {
1018
+ // TODO: Optimize this into a Set or Map or something?
1019
+ if (!this.existingTextNodes)
1020
+ this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
1021
+
1022
+ let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
1023
+ if (idx !== -1)
1024
+ newNodes.push(...this.existingTextNodes.splice(idx, 1));
1025
+ else
1026
+ newNodes.push(this.parentNode.ownerDocument.createTextNode(text));
1027
+ }
1028
+ }
1029
+ }
1030
+
740
1031
  applyMultipleAttribs(node, expr) {
741
1032
  /*#IFDEV*/assert(this.type === PathType.Multiple);/*#ENDIF*/
742
1033
 
@@ -797,7 +1088,6 @@ class ExprPath {
797
1088
  func = setValue;
798
1089
  args = [expr[0], expr.slice(1), node];
799
1090
  node.value = delve(expr[0], expr.slice(1));
800
- //debugger;
801
1091
  }
802
1092
  }
803
1093
  else
@@ -918,8 +1208,7 @@ class ExprPath {
918
1208
  * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
919
1209
  * share the same DOM parent node.
920
1210
  *
921
- * TODO: Is recursive clearing ever necessary?
922
- */
1211
+ * TODO: Is recursive clearing ever necessary? */
923
1212
  clearNodesCache() {
924
1213
  let path = this;
925
1214
 
@@ -937,7 +1226,7 @@ class ExprPath {
937
1226
 
938
1227
  // Clear cache of child ExprPaths that have the same parentNode
939
1228
  for (let ng of path.nodeGroups) {
940
- if (ng) // Can be null from applyOneExpr()'s push(null) call.
1229
+ if (ng) // Can be null from apply()'s push(null) call.
941
1230
  for (let path2 of ng.paths) {
942
1231
  if (path2.type === PathType.Content && path2.parentNode === parentNode) {
943
1232
  path2.nodesCache = null;
@@ -1205,7 +1494,7 @@ class Shell {
1205
1494
 
1206
1495
  // Swap out Embedded Solarite Components with ${} attributes.
1207
1496
  // Later, NodeGroup.render() will search for these and replace them with the real components.
1208
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1497
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1209
1498
  if (context === htmlContext.Attribute) {
1210
1499
 
1211
1500
  let lastIndex, lastMatch;
@@ -1215,7 +1504,7 @@ class Shell {
1215
1504
  });
1216
1505
 
1217
1506
  if (lastMatch) {
1218
- let newTagName = lastMatch + '-redcomponent-placeholder';
1507
+ let newTagName = lastMatch + '-solarite-placeholder';
1219
1508
  lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
1220
1509
  componentNames[lastMatch] = newTagName;
1221
1510
  }
@@ -1232,9 +1521,9 @@ class Shell {
1232
1521
 
1233
1522
  // 2. Create elements from html with placeholders.
1234
1523
  let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
1235
- let joinedHtml = buffer.join('');
1236
-
1237
- // Replace '-redcomponent-placeholder' close tags.
1524
+ let joinedHtml = buffer.join('');
1525
+
1526
+ // Replace '-solarite-placeholder' close tags.
1238
1527
  // TODO: is there a better way? What if the close tag is inside a comment?
1239
1528
  for (let name in componentNames)
1240
1529
  joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
@@ -1257,7 +1546,7 @@ class Shell {
1257
1546
 
1258
1547
  // Replace attributes
1259
1548
  if (node.nodeType === 1) {
1260
- for (let attr of node.attributes) {
1549
+ for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
1261
1550
 
1262
1551
  // Whole attribute
1263
1552
  let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
@@ -1359,8 +1648,8 @@ class Shell {
1359
1648
  }
1360
1649
  toRemove.map(el => el.remove());
1361
1650
 
1362
- // Handle redcomponent-placeholder's.
1363
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1651
+ // Handle solarite-placeholder's.
1652
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1364
1653
  //if (componentNames.size)
1365
1654
  // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
1366
1655
 
@@ -1455,87 +1744,6 @@ class Shell {
1455
1744
 
1456
1745
  let shells = new WeakMap();
1457
1746
 
1458
- /**
1459
- * The html strings and evaluated expressions from an html tagged template.
1460
- * A unique Template is created for each item in a loop.
1461
- * Although the reference to the html strings is shared among templates. */
1462
- class Template {
1463
-
1464
- /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
1465
- exprs = []
1466
-
1467
- /** @type {string[]} */
1468
- html = [];
1469
-
1470
- /**
1471
- * If true, use this template to replace an existing element, instead of appending children to it.
1472
- * @type {?boolean} */
1473
- replaceMode;
1474
-
1475
- /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
1476
- hashedFields;
1477
-
1478
- /**
1479
- * @deprecated
1480
- * @type {ExprPath} Used with forEach() from watch.js
1481
- * Set in NodeGroup.applyOneExpr() */
1482
- parentPath;
1483
-
1484
- /** @type {NodeGroup} */
1485
- nodeGroup;
1486
-
1487
- /**
1488
- * @type {string[][]} */
1489
- paths = [];
1490
-
1491
- /**
1492
- *
1493
- * @param htmlStrings {string[]}
1494
- * @param exprs {*[]} */
1495
- constructor(htmlStrings, exprs) {
1496
- this.html = htmlStrings;
1497
- this.exprs = exprs;
1498
-
1499
- //this.trace = new Error().stack.split(/\n/g)
1500
-
1501
- // Multiple templates can share the same htmlStrings array.
1502
- //this.hashedFields = [getObjectId(htmlStrings), exprs]
1503
-
1504
- //#IFDEV
1505
- assert(Array.isArray(htmlStrings));
1506
- assert(Array.isArray(exprs));
1507
-
1508
- Object.defineProperty(this, 'debug', {
1509
- get() {
1510
- return JSON.stringify([this.html, this.exprs]);
1511
- }
1512
- });
1513
- //#ENDIF
1514
- }
1515
-
1516
- /**
1517
- * Called by JSON.serialize when it encounters a Template.
1518
- * This prevents the hashed version from being too large. */
1519
- toJSON() {
1520
- if (!this.hashedFields)
1521
- this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
1522
-
1523
- return this.hashedFields
1524
- }
1525
-
1526
- toNode() {
1527
- let ngm = new NodeGroupManager();
1528
- return ngm.render(this);
1529
- }
1530
-
1531
- getCloseKey() {
1532
- // Use the joined html when debugging?
1533
- //return '@'+this.html.join('|')
1534
-
1535
- return '@'+this.hashedFields[0];
1536
- }
1537
- }
1538
-
1539
1747
  /**
1540
1748
  * ISC License
1541
1749
  *
@@ -1755,734 +1963,16 @@ const udomdiff = (parentNode, a, b, before) => {
1755
1963
  parentNode.removeChild(aNode);
1756
1964
 
1757
1965
  //#IFDEV
1758
- if (aNode instanceof NodeGroup)
1759
- aNode.verify();
1760
- // if (parentNode instanceof ExprPath)
1761
- // parentNode.verify();
1762
- //#ENDIF
1763
- }
1764
- }
1765
- }
1766
- return b;
1767
- };
1768
-
1769
- /**
1770
- * Tools for watch variables and performing precise renders.
1771
- */
1772
-
1773
-
1774
- /**
1775
- * Stores info how to transform a path to a template. */
1776
- class TransformerInfo {
1777
- constructor(path, transformer, hash) {
1778
- this.path = path;
1779
- this.transformer = transformer;
1780
- this.hash = hash;
1781
- }
1782
- }
1783
-
1784
-
1785
- /**
1786
- * Maps an object path to the function that converts it to a Template.
1787
- * Once it's convert to a template, we can get the hash of that Tempate.
1788
- * Then that hash tells us what NodeGroups are affected by the object.
1789
- *
1790
- * We store the function to get the Template, instead of the Template itself,
1791
- * so we can call that function again when the object has a new value.
1792
- * @type {MultiValueMap<Object, function(...Object):Template>} */
1793
- let pathToTransformer = new MultiValueMap(); // uses a Set() for each value.
1794
-
1795
- /**
1796
- *
1797
- * @param objectPaths {(*|function)[]}
1798
- * @returns {Template} */
1799
- function watchGet(...objectPaths) {
1800
-
1801
- /** @type {function} */
1802
- let transformer = objectPaths.at(-1);
1803
- let paths;
1804
- if (typeof transformer === 'function') {
1805
- paths = objectPaths.slice(0, -1);
1806
- }
1807
-
1808
- // No transformer provided, so we create our own.
1809
- else if (objectPaths.length === 1) {
1810
- paths = [objectPaths[0].slice(0, -1)];
1811
- let prop = objectPaths[0].at(-1);
1812
- transformer = (...args) => (args[0][prop]);
1813
- }
1814
-
1815
-
1816
- // Save arguments used to call the template, so we can call it again when those args have their values change.
1817
- let args = [];
1818
- for (let path of paths) {
1819
-
1820
-
1821
- let obj = delve(watchSet(path[0]), path.slice(1));
1822
- args.push(obj);
1823
- }
1824
-
1825
- let template = transformer(...args);
1826
-
1827
- // If the result isn't a Template, convert the function to return a Template that wraps the result.
1828
- // This way NodeGroupManager.findAndDelete() can find a NodeGroup that matches this Template's hash.
1829
- if (!(template instanceof Template)) {
1830
- let oldToTemplate = transformer;
1831
- transformer = function() {
1832
- return new Template(['', ''], [oldToTemplate(...arguments)]);
1833
- };
1834
- template = transformer(...args);
1835
- }
1836
-
1837
- // Map the object paths to the function that creates a template.
1838
- let hash = getObjectHash(template);
1839
- for (let path of paths) {
1840
- let serializedPath = serializePath(path);
1841
- pathToTransformer.add(serializedPath, new TransformerInfo(path, transformer, hash)); // Uses a Set() to ensure no duplicates.
1842
- }
1843
-
1844
- return template;
1845
- }
1846
-
1847
- //let proxyCache = new WeakMap();
1848
-
1849
-
1850
- /**
1851
- * Set the value of a variable in a way that's watched, so later when we call .renderWatched()
1852
- * We can find what NodeGroups to update.*/
1853
- function watchSet(obj) {
1854
- if (obj?.$isProxy===true)
1855
- return obj; // It's already a Proxy.
1856
-
1857
- // This cache doesn't make things faster.
1858
- // let result = proxyCache.get(obj);
1859
- // if (!result) {
1860
- // result = new Proxy(obj, new ProxyHandler(obj));
1861
- // proxyCache.set(obj, result);
1862
- // }
1863
- // return result;
1864
- return new Proxy(obj, new ProxyHandler$1(obj));
1865
- }
1866
-
1867
- /**
1868
- * Loop over each item and apply watchGet() to each item.
1869
- * @param arrayPath {*[]}
1870
- * @param callback {function(obj:Object, index:int):Template}
1871
- * @returns {Template} */
1872
- function forEach(arrayPath, callback) {
1873
- let array = delve(arrayPath[0], arrayPath.slice(1));
1874
-
1875
- // This is retrieved on the 'insert' path inside renderWatched()
1876
- let ngm = NodeGroupManager.get(arrayPath[0]);
1877
- if (ngm.clearSubscribers) {
1878
- ngm.clearSubscribers = false;
1879
- ngm.pathToLoopInfo = new MultiValueMap();
1880
- } // TODO: Move tis into NodeGroupMAnager.get() without breaking things?
1881
-
1882
-
1883
- let newItems = [...array.map((item, i) => {
1884
- // TODO: This needs to wrap callback so we can pass it the index also.
1885
- return watchGet([...arrayPath, i], callback); // calls callback(array[i], i)
1886
- })
1887
- ];
1888
-
1889
- // We return a template that wraps the array
1890
- // So that NodeGroup.applyOneExpr can set the ExprPath and nextSibling on the template.
1891
- // Then the 'insert' path in renderWatched() uses that data fora dding more nodes.
1892
- let result = new Template(['', ''], [newItems]);
1893
-
1894
-
1895
- // We get a unique hash for each foreach template because the [''] array is unique each time.
1896
- let loopInfo = new LoopInfo(result, callback);
1897
- ngm.pathToLoopInfo.add(serializePath(arrayPath), loopInfo);
1898
- return result;
1899
- }
1900
-
1901
- function serializePath(path) {
1902
- // Convert any array indices to strings, so serialized comparisons work.
1903
- return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
1904
-
1905
- }
1906
-
1907
-
1908
- /**
1909
- * When an object property is accessed, a new Proxy with a new instance of this handler class is created,
1910
- * but it tracks the path from the root to the property.
1911
- * That way when a property is set, it can report the changed path. */
1912
- class ProxyHandler$1 {
1913
-
1914
- /**
1915
- * @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
1916
- * @param path {string[]} Used internally. */
1917
- constructor(root, path=[]) {
1918
- /*#IFDEV*/assert(NodeGroupManager.get(root));/*#ENDIF*/
1919
- this.root = root;
1920
- this.path = path; // path from root to this Proxy.
1921
- }
1922
-
1923
- /**
1924
- * @param obj {Object}
1925
- * @param prop {string} */
1926
- get(obj, prop) {
1927
-
1928
- // Special props. Currently unused.
1929
- // if (prop === '$path')
1930
- // return this.path;
1931
- // if (prop === '$root')
1932
- // return this.root;
1933
- if (prop === '$isProxy')
1934
- return true;
1935
-
1936
-
1937
- // 1. Array.splice()
1938
- if (prop === 'splice' && Array.isArray(obj)) {
1939
- return (index, deleteCount, ...items) => {
1940
- let ngm = NodeGroupManager.get(this.root);
1941
-
1942
- if (deleteCount) {
1943
-
1944
- // Get the hash of each object along the delete range. The process to get the hash is:
1945
- // Serialized Path -> transformer -> Template -> hash.
1946
- let hashes = [];
1947
- for (let i=index; i<index+deleteCount; i++) {
1948
- let serializedPath = serializePath([this.root, ...this.path, i+'']);
1949
-
1950
- let obj = delve(this.root, [...this.path, i]);
1951
- for (let transformerInfo of pathToTransformer.getAll(serializedPath)) {
1952
- let template = transformerInfo.transformer(obj);
1953
- let hash = getObjectHash(template);
1954
- hashes.push(hash); // Hashes may go to nodes in more than one loop.
1955
- }
1956
- }
1957
-
1958
- ngm.changes.push(new Change('delete', this.root, [...this.path, index+''], hashes));
1959
- }
1960
-
1961
- //let oldArray = [...obj];
1962
- let result = obj.splice(index, deleteCount);
1963
-
1964
- // Inserting
1965
- if (items.length) {
1966
-
1967
- let beforeNgs;
1968
- for (let loopInfo of ngm.getLoopInfo([this.root, ...this.path])) {
1969
- let beforeObj = delve(this.root, [...this.path, index]);
1970
-
1971
- // Find where to insert before.
1972
- if (beforeObj) {
1973
- let beforeTemplate = loopInfo.itemTransformer(beforeObj);
1974
- let beforeHash = getObjectHash(beforeTemplate);
1975
- beforeNgs = ngm.nodeGroupsAvailable.data[beforeHash];
1976
-
1977
- if (beforeNgs) {
1978
- let hash = getObjectHash(loopInfo.template);
1979
- let loopNgs = ngm.nodeGroupsAvailable.data[hash] || [];
1980
- for (let loopNg of loopNgs)
1981
- for (let beforeNg of beforeNgs)
1982
- if (beforeNg.startNode.parentNode === loopNg.startNode.parentNode)
1983
- ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items, beforeTemplate));
1984
- }
1985
- }
1986
- if (!beforeNgs)
1987
- ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items));
1988
- }
1989
- obj.splice(index, 0, ...items);
1990
- }
1991
- return result;
1992
- }
1993
- }
1994
-
1995
-
1996
-
1997
- // 2. Get property
1998
- // If we're getting an object or array property, apply watch() to it recursively.
1999
- let result = Reflect.get(obj, prop);
2000
- if (result && typeof result === 'object') {
2001
- let handler = new ProxyHandler$1(this.root, [...this.path, prop]); // same root, one level deeper on the path.
2002
- return new Proxy(result, handler);
2003
- }
2004
-
2005
- return result;
2006
- }
2007
-
2008
-
2009
- set(obj, prop, newValue) {
2010
- let ngm = NodeGroupManager.get(this.root);
2011
- ngm.changes.push(new Change('set', this.root, [...this.path, prop], newValue));
2012
- return Reflect.set(obj, prop, newValue)
2013
- }
2014
- }
2015
-
2016
-
2017
- /**
2018
- *
2019
- */
2020
- class Change {
2021
-
2022
- /**
2023
- * @param action {string}
2024
- * @param root {Object|Array}
2025
- * @param path {string[]}
2026
- * @param value
2027
- * If setting a value, this is the new value.
2028
- * If deleting from an array, this is an array of all the NodeGroups to delete.
2029
- *
2030
- * @param beforeTemplate
2031
- * */
2032
- constructor(action, root, path, value, beforeTemplate=null) {
2033
- this.action = action;
2034
-
2035
- // TODO: Store root as first item of path, to be consistent with code elsewhere.
2036
- this.root = root;
2037
- this.path = path;
2038
- this.value = value;
2039
- this.beforeTemplate = beforeTemplate;
2040
-
2041
- /** @type {TransformerInfo[]} */
2042
- this.transformerInfo = [];
2043
-
2044
- // Traverse up the path.
2045
- for (let i=this.path.length; i>0; i--) {
2046
- let path = this.path.slice(0, i);
2047
- let fullPath = [this.root, ...path];
2048
-
2049
- let serializedPath = getObjectHash(fullPath); // TODO: Why not serializedPath() ?
2050
- this.transformerInfo.push(...pathToTransformer.getAll(serializedPath));
2051
- }
2052
- }
2053
- }
2054
-
2055
- let logGets = false;
2056
- let gets = [];
2057
-
2058
- let withinSet = 0;
2059
-
2060
- /**
2061
- * Turn the props on obj into JavasCript properties that return Proxies when accessed.
2062
- * If called more than once, return the already-converted object.
2063
- * @param obj {Object}
2064
- * @param props {string}
2065
- * @returns {*|{$proxyHandler}}
2066
- */
2067
- function watch(obj, ...props) {
2068
-
2069
- if (props.length) {
2070
- let internalProps = {};
2071
- for (let prop of props) {
2072
- internalProps[prop] = obj[prop];
2073
- Object.defineProperty(obj, prop, {
2074
- get() {
2075
- return new Proxy(obj, new ProxyHandler(obj, [], internalProps))[prop];
2076
- },
2077
- set(value) {
2078
- return watch(this)[prop] = value;
2079
- }
2080
- });
2081
- }
2082
- return;
2083
- }
2084
-
2085
-
2086
- if (obj?.$proxyHandler)
2087
- return obj; // It's already a Proxy.
2088
-
2089
- // This cache doesn't make things faster.
2090
- // But could it save memory?
2091
- // let result = proxyCache.get(obj);
2092
- // if (!result) {
2093
- // result = new Proxy(obj, new ProxyHandler(obj));
2094
- // proxyCache.set(obj, result);
2095
- // }
2096
- // return result;
2097
- /*#IFDEV*/assert(!obj.$proxyHandler);/*#ENDIF*/
2098
- return new Proxy(obj, new ProxyHandler(obj));
2099
- }
2100
-
2101
- /**
2102
- * Provides methods used when a Proxied version of a property is accessed on an object returned by watch() */
2103
- class ProxyHandler {
2104
-
2105
- serializedPath;
2106
-
2107
- /**
2108
- * @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
2109
- * @param path {string[]} Used internally.
2110
- * @param props */
2111
- constructor(root, path=[], props=null) {
2112
- /*#IFDEV*/assert(NodeGroupManager.get(root));/*#ENDIF*/
2113
- this.root = root;
2114
- this.path = path; // path from root to this Proxy.
2115
- this.props = props;
2116
- }
2117
-
2118
- /**
2119
- * Get the full path to this property from the root watched object.
2120
- * @param atIndex
2121
- * @returns {string} */
2122
- getSerializedPath(atIndex=null) {
2123
- if (!this.serializedPath)
2124
- this.serializedPath = JSON.stringify([getObjectId(this.root), ...this.path.map(item => item + '')]);
2125
-
2126
- if (atIndex!== null)
2127
- return this.serializedPath.slice(0, -1) + ',"' + atIndex + '"]';
2128
- return this.serializedPath;
2129
- }
2130
-
2131
- /**
2132
- * Return a ProxyHandler for a property one level deeper at pathItem.
2133
- * @param pathItem {string}
2134
- * @returns {ProxyHandler} */
2135
- extend(pathItem) {
2136
- pathItem += '';
2137
- assert(!this.root.$proxyHandler);
2138
- let result = new ProxyHandler(this.root, [...this.path, pathItem], this.props);
2139
- if (this.serializedPath)
2140
- result.serializedPath = this.getSerializedPath(pathItem);
2141
-
2142
- return result;
2143
- }
2144
-
2145
- /**
2146
- * Called directly by JavaScript when accessing the value of a property.
2147
- * @param obj {Object}
2148
- * @param prop {string} */
2149
- get(obj, prop) {
2150
-
2151
- // 1. Special props.
2152
- if (prop === '$proxyHandler')
2153
- return this;
2154
- else if (prop === '$removeProxy')
2155
- return delve(this.props || this.root, this.path);
2156
-
2157
- // 2. Array functions.
2158
- else if (prop === 'map' && Array.isArray(obj)) {
2159
-
2160
- let ngm = NodeGroupManager.get(this.root);
2161
- ngm.clearSubscribersIfNeeded();
2162
-
2163
- return callback => {
2164
- let loopInfo;
2165
-
2166
- let children = [];
2167
- let transformer = obj => {
2168
- let templates = [];
2169
-
2170
- for (let i = 0; i < obj.length; i++) {
2171
-
2172
- // Watch obj[i].
2173
- let handler = this.extend(i);
2174
- let item = obj[i];
2175
- if (!item.$proxyHandler)
2176
- item = new Proxy(item, handler);
2177
-
2178
- let template = callback(item, i, obj);
2179
- templates.push(template);
2180
-
2181
- // If the loop is re-evaluted via Set() then we add duplicate TemplateInfo's
2182
- //if (!withinSet) {
2183
- let spath = this.getSerializedPath(i);
2184
- let subscriber = new Subscriber(callback, template);
2185
- subscriber.parent = loopInfo;
2186
- ngm.subscribers.add(spath, subscriber);
2187
- children.push([spath, subscriber]);
2188
- //}
2189
- }
2190
-
2191
- // A parent Template that surrounds all the items in the loop.
2192
- // This lets us get template.nodeGroup.endNode so we can insertBefore().
2193
- return new Template(['', ''], [templates]);
2194
- };
2195
-
2196
- if (!withinSet)
2197
- loopInfo = new Subscriber(transformer, null, callback);
2198
- let wholeLoopTemplate = transformer(obj);
2199
- if (!withinSet) {
2200
- loopInfo.template = wholeLoopTemplate;
2201
- loopInfo.children = children;
2202
- ngm.subscribers.add(this.getSerializedPath(), loopInfo);
2203
- }
2204
- return wholeLoopTemplate;
2205
- }
2206
- }
2207
-
2208
- else if ((prop ==='splice' || prop === 'fastSplice') && Array.isArray(obj)) {
2209
- let ngm = NodeGroupManager.get(this.root);
2210
- return (index, deleteCount, ...items) => {
2211
- let diff = items.length - deleteCount;
2212
- let objLength = obj.length;
2213
-
2214
- // Delete
2215
- if (deleteCount) {
2216
- for (let i=index; i<index+deleteCount; i++) {
2217
-
2218
- // Update pathToTemplates
2219
- let spath = this.getSerializedPath(i);
2220
-
2221
- // Delete nodes of associated NodeGroups.
2222
- for (let subscriber of ngm.subscribers.data[spath] || []) {
2223
- let ng = subscriber.template.nodeGroup;
2224
- for (let node of ng.getNodes())
2225
- node.remove();
2226
-
2227
- // Delete NodeGroup from NodeGroupManager.
2228
- ngm.nodeGroupsAvailable.delete(ng.exactKey, ng);
2229
- }
2230
-
2231
- delete ngm.subscribers.data[spath]; // Deletes templates associated with every loop where this is used.
2232
- }
2233
- }
2234
-
2235
- // Update indices of subsequent items.
2236
- if (diff) {
2237
- let loopPath = this.getSerializedPath();
2238
- let loopInfo = [...ngm.subscribers.getAll(loopPath)][0]; // TODO: Handle multiple loops.
2239
-
2240
- let move = (oldIndex) => {
2241
- let newIndex = oldIndex+diff;
2242
- let oldPath = this.getSerializedPath(oldIndex);
2243
- let newPath = this.getSerializedPath(newIndex);
2244
-
2245
- let subscribers = ngm.subscribers.data[oldPath];
2246
- delete ngm.subscribers.data[oldPath]; // TODO: Some can be overwritten w/o being deleted?
2247
- ngm.subscribers.data[newPath] = subscribers;
2248
-
2249
-
2250
- // Update associated NodeGroups by passing them newIndex.
2251
- // This is unnecessary for most loops since they don't use the index.
2252
- // fastSplice skips this path, it skips updating item indices.
2253
- if (prop === 'splice') {
2254
- let array = delve(this.root, this.path);
2255
- for (let subscriber of subscribers) {
2256
- let ng = subscriber.template.nodeGroup;
2257
-
2258
- let item = array[oldIndex];
2259
-
2260
- //assert(!item.$proxyHandler)
2261
- let proxyItem = getProxy(item, this, this.path, newIndex); //new Proxy(item, this.extend(newIndex));
2262
- let exprs = loopInfo.itemTransformer(proxyItem, newIndex).exprs; // TODO: Pass updated array as third argument to transformer.
2263
- ng.applyExprs(exprs); // this is the slow part.
2264
- }
2265
- }
2266
- };
2267
-
2268
- // Iterate in different directions depending on whether diff is positive or negative.
2269
- if (diff > 0) // Moving items to the right, so we iterate backward from the end.
2270
- for (let i = objLength-1; i >= index + items.length + deleteCount; i--)
2271
- move(i);
2272
-
2273
- else // Moving items to the left, so we iterate forward.
2274
- for (let i = index + items.length + deleteCount; i < objLength; i++)
2275
- move(i);
2276
- }
2277
-
2278
-
2279
- // Add new items
2280
- if (items.length) {
2281
- let loopPath = this.getSerializedPath();
2282
-
2283
- let beforePath = this.getSerializedPath(index);
2284
- let ngm = NodeGroupManager.get(this.root);
2285
-
2286
- for (let loopInfo of ngm.subscribers.getAll(loopPath)) {
2287
- let beforeNodes = index < objLength - deleteCount
2288
- ? [...ngm.subscribers.getAll(beforePath)].map(t => t.template.nodeGroup.startNode)
2289
- : [loopInfo.template.nodeGroup.endNode];
2290
- for (let beforeNode of beforeNodes) { // TODO: Need to match the beforeNg with the loopInfo instead of iterating.
2291
- for (let i = 0; i < items.length; i++) {
2292
-
2293
- // Create NodeGroup of new item.
2294
- let itemHandler = this.extend(index + i);
2295
- assert(!items[i].$proxyHandler);
2296
- let proxyItem = new Proxy(items[i], itemHandler);
2297
- let template = loopInfo.itemTransformer(proxyItem);
2298
- let ng = ngm.getNodeGroup(template, null, true);
2299
-
2300
-
2301
- for (let node of ng.getNodes()) {
2302
- beforeNode.parentNode.insertBefore(node, beforeNode);
2303
- //loopInfo.template.nodeGroup.endNode = node; // The loop's end node is actually an empty text node, so don't do this.
2304
- }
2305
-
2306
-
2307
- // Add new items to ngm.templateInfo
2308
- let spath = itemHandler.getSerializedPath();
2309
- let subscriber = new Subscriber(loopInfo.itemTransformer, template);
2310
- subscriber.parent = loopInfo;
2311
- ngm.subscribers.add(spath, subscriber);
2312
- }
2313
- }
2314
- loopInfo.template.nodeGroup.parentPath.clearNodesCache();
2315
- loopInfo.template.nodeGroup.nodesCache = null;
2316
- }
2317
- }
2318
-
2319
- let result = obj.splice(index, deleteCount, ...items);
2320
-
2321
- //this.notify(this.path);
2322
-
2323
- return result;
2324
- }
2325
- }
2326
-
2327
- // Allow these functions to use proxied objects as arguments.
2328
- else if (prop ==='indexOf' && Array.isArray(obj)) {
2329
- return item => {
2330
- return obj.indexOf(item.$removeProxy || item)
2331
- }
2332
- }
2333
-
2334
- // 3. Get property
2335
- else {
2336
-
2337
- let obj2 = obj === this.root && this.props ? this.props : obj;
2338
- let result = Reflect.get(obj2, prop);
2339
-
2340
- // This is read by watchFunction() which is called in NodeGroup.applyOneExpr().
2341
- // It's used to see what variables contribute to an expression.
2342
- if (logGets)
2343
- gets.push([this.root, ...this.path, prop]);
2344
-
2345
- // If we're getting an object or array property, apply watch() to it recursively.
2346
- if (result && typeof result === 'object') {
2347
- let handler = this.extend(prop); // same root, one level deeper on the path.
2348
- /*#IFDEV*/assert(!result.$proxyHandler);/*#ENDIF*/
2349
- return new Proxy(result, handler);
2350
- }
2351
-
2352
- return result;
2353
- }
2354
- }
2355
-
2356
-
2357
- /**
2358
- * Called directly by JavaScript when setting the value of a property via equals.
2359
- * @param obj
2360
- * @param prop
2361
- * @param value
2362
- * @returns {boolean} */
2363
- set(obj, prop, value) {
2364
- withinSet++;
2365
-
2366
- let obj2 = obj === this.root && this.props ? this.props : obj;
2367
- let result = Reflect.set(obj2, prop, value);
2368
- let fullPath = [...this.path, prop+''];
2369
- this.notify(fullPath);
2370
-
2371
- withinSet --;
2372
-
2373
- return result;
2374
- }
2375
-
2376
- /**
2377
- * Find every subscriber for fullPath, and above, and call applyExprs() for it.
2378
- * @param fullPath {string[]}
2379
- * @param excluded {Set} */
2380
- notify(fullPath, excluded = new Set()) {
2381
-
2382
- // Traverse upward through the path, looking for pathToTemplates.
2383
- let ngm = NodeGroupManager.get(this.root);
2384
-
2385
- let rootHash = getObjectId(this.root);
2386
-
2387
- let len = fullPath.length;
2388
- while (len >= 1) {
2389
- let path = fullPath.slice(0, len);
2390
- let val = delve(this.root, path);
2391
- let serializedPath = JSON.stringify([rootHash, ...path]);
2392
- for (let subscriber of ngm.subscribers.getAll(serializedPath)) {
2393
- // We already applied expressions for a single item within this loop.
2394
- if (excluded.has(subscriber))
2395
- continue;
2396
-
2397
-
2398
- // Delete child subscriptions so we don't have duplicate subscriptions when we call applyExprs() directly below.
2399
- if (subscriber.children) {
2400
- for (let [spath, childInfo] of subscriber.children)
2401
- ngm.subscribers.delete(spath, childInfo);
2402
- subscriber.children = undefined;
2403
- }
2404
-
2405
- let proxyVal = getProxy(val, this, path);
2406
- let exprs = subscriber.transformer(proxyVal).exprs; // TODO: Pass updated array as third argument to transformer.
2407
- for (let path of subscriber.template.nodeGroup.paths)
2408
- path.clearNodesCache();
2409
-
2410
- // Apply expressions.
2411
- subscriber.template.nodeGroup.applyExprs(exprs);
2412
-
2413
- // Don't also process parent loop after updating a single item within it.
2414
- if (subscriber.parent)
2415
- excluded.add(subscriber.parent);
1966
+ if (aNode instanceof NodeGroup)
1967
+ aNode.verify();
1968
+ // if (parentNode instanceof ExprPath)
1969
+ // parentNode.verify();
1970
+ //#ENDIF
2416
1971
  }
2417
- len--;
2418
1972
  }
2419
1973
  }
2420
- }
2421
-
2422
- function getProxy(obj, ph, path, path2) {
2423
- if (!obj || !typeof obj !== 'object')
2424
- return obj;
2425
-
2426
- if (obj.$proxyHandler) {
2427
- //#IFDEV
2428
- if (path2)
2429
- path = [...path, path2+''];
2430
- //#ENDIF
2431
- assert(obj.$proxyHandler.root === ph.root && JSON.stringify(obj.$proxyHandler.path) === JSON.stringify(path));
2432
- return obj;
2433
- }
2434
- if (path2)
2435
- path = [...path, path2+''];
2436
- return new Proxy(obj, new ProxyHandler(ph.root, path, ph.props));
2437
- }
2438
-
2439
- /**
2440
- * Call a function and record which watched variables it accesess, storing their paths in pathToTemplates.
2441
- * Used by NodeGroup.applyOneExpr().
2442
- * TODO: only allow this to be called once per callback.
2443
- * @param callback {function}
2444
- * @param ngm {NodeGroupManager}
2445
- * @returns {Template} */
2446
- function watchFunction(callback, ngm) {
2447
- ngm.clearSubscribersIfNeeded();
2448
-
2449
- logGets = true;
2450
-
2451
- let transformer = () => new Template(['', ''], [callback()]);
2452
- let template = transformer();
2453
- for (let path of gets) {
2454
- let subscriber = new Subscriber(transformer, template);
2455
- ngm.subscribers.add(serializePath(path), subscriber);
2456
- }
2457
-
2458
- gets = [];
2459
- logGets = false;
2460
- return template;
2461
- }
2462
-
2463
- /**
2464
- * Represents a place where nodes will be updated.
2465
- * TODO: Merge this with Template, or ExprPath? */
2466
- class Subscriber {
2467
-
2468
- /** @type {Subscriber} Used only for children of a loop. */
2469
- parent;
2470
-
2471
- /** @type {Subscriber[]} TemplateInfo for each child of a loop. */
2472
- children;
2473
-
2474
- /**
2475
- * @param transformer {function} Function that turns the object at the path into a template.
2476
- * @param template {Template}
2477
- * @param itemTransformer {function} If a loop, this transforms each item in the loop. */
2478
- constructor(transformer, template, itemTransformer=null) {
2479
- this.transformer = transformer;
2480
- this.template = template;
2481
-
2482
- // Used only for loops
2483
- this.itemTransformer = itemTransformer;
2484
- }
2485
- }
1974
+ return b;
1975
+ };
2486
1976
 
2487
1977
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
2488
1978
 
@@ -2513,10 +2003,10 @@ class NodeGroup {
2513
2003
  /** @type {ExprPath[]} */
2514
2004
  paths = [];
2515
2005
 
2516
- /** @type {string} */
2006
+ /** @type {string} Key that matches the template and the expressions. */
2517
2007
  exactKey;
2518
2008
 
2519
- /** @type {string} */
2009
+ /** @type {string} Key that only matches the template. */
2520
2010
  closeKey;
2521
2011
 
2522
2012
  /** @type {boolean} Used by NodeGroupManager. */
@@ -2549,8 +2039,10 @@ class NodeGroup {
2549
2039
  * @returns {NodeGroup} */
2550
2040
  constructor(template, manager=null) {
2551
2041
 
2552
- // Used for forEach()
2042
+ /** @type {Template} */
2553
2043
  this.template = template;
2044
+
2045
+ /** @type {NodeGroupManager} */
2554
2046
  this.manager = manager;
2555
2047
 
2556
2048
  // new!
@@ -2566,7 +2058,7 @@ class NodeGroup {
2566
2058
  let replaceMode = typeof template.replaceMode === 'boolean'
2567
2059
  ? template.replaceMode
2568
2060
  : fragment.children.length===1 &&
2569
- fragment.firstElementChild?.tagName.replace(/-REDCOMPONENT-PLACEHOLDER$/, '')
2061
+ fragment.firstElementChild?.tagName.replace(/-SOLARITE-PLACEHOLDER$/, '')
2570
2062
  === manager?.rootEl?.tagName;
2571
2063
  if (replaceMode) {
2572
2064
  this.pseudoRoot = fragment.firstElementChild;
@@ -2593,7 +2085,7 @@ class NodeGroup {
2593
2085
 
2594
2086
 
2595
2087
  // Update web component placeholders.
2596
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
2088
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2597
2089
  // Is this list needed at all?
2598
2090
  //for (let component of shell.components)
2599
2091
  // this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
@@ -2626,13 +2118,19 @@ class NodeGroup {
2626
2118
  this.createNewComponent(el);
2627
2119
  }
2628
2120
 
2629
- if (this.manager.rootEl) {
2121
+ if (this.manager?.rootEl) {
2630
2122
 
2631
2123
  // ids
2632
2124
  if (this.manager.options.ids !== false)
2633
2125
  for (let path of shell.ids) {
2634
2126
  let el = resolveNodePath(root, path);
2635
2127
  let id = el.getAttribute('data-id') || el.getAttribute('id');
2128
+
2129
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
2130
+ if (this.manager.rootEl[id] && !(this.manager.rootEl[id] instanceof Node))
2131
+ throw new Error(`${this.manager.rootEl.constructor.name}.${id} already has a value. `+
2132
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
2133
+
2636
2134
  this.manager.rootEl[id] = el;
2637
2135
  }
2638
2136
 
@@ -2657,20 +2155,13 @@ class NodeGroup {
2657
2155
  }
2658
2156
  }
2659
2157
 
2660
- updateStyles() {
2661
- if (this.styles)
2662
- for (let [style, oldText] of this.styles) {
2663
- let newText = style.textContent;
2664
- if (oldText !== newText)
2665
- Util.bindStyles(style, this.manager.rootEl);
2666
- }
2667
- }
2668
-
2669
2158
  /**
2670
2159
  * Use the paths to insert the given expressions.
2671
2160
  * Dispatches expression handling to other functions depending on the path type.
2672
- * @param exprs {(*|*[]|function|Template)[]} */
2673
- applyExprs(exprs) {
2161
+ * @param exprs {(*|*[]|function|Template)[]}
2162
+ * @param paths {?ExprPath[]} Optional. */
2163
+ applyExprs(exprs, paths=null) {
2164
+ paths = paths || this.paths;
2674
2165
 
2675
2166
  /*#IFDEV*/this.verify();/*#ENDIF*/
2676
2167
  // Update exprs at paths.
@@ -2678,7 +2169,7 @@ class NodeGroup {
2678
2169
 
2679
2170
  // We apply them in reverse order so that a <select> box has its options created from an expression
2680
2171
  // before its value attribute is set via an expression.
2681
- for (let path of this.paths.toReversed()) {
2172
+ for (let path of paths.toReversed()) {
2682
2173
  expr = exprs[exprIndex];
2683
2174
 
2684
2175
  // Nodes
@@ -2689,8 +2180,8 @@ class NodeGroup {
2689
2180
 
2690
2181
  // Attributes
2691
2182
  else {
2692
- let node = path.nodeMarker; // path.resolve(result);
2693
- let node2 = (this.manager.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
2183
+ let node = path.nodeMarker;
2184
+ let el = (this.manager?.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
2694
2185
  /*#IFDEV*/assert(node);/*#ENDIF*/
2695
2186
 
2696
2187
  // This is necessary both here and below.
@@ -2700,10 +2191,10 @@ class NodeGroup {
2700
2191
  }
2701
2192
 
2702
2193
  if (path.type === PathType.Multiple)
2703
- path.applyMultipleAttribs(node2, expr);
2194
+ path.applyMultipleAttribs(el, expr);
2704
2195
 
2705
2196
  // Capture attribute expressions to later send to the constructor of a web component.
2706
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
2197
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2707
2198
  else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
2708
2199
  this.currentComponentProps[path.attrName] = expr;
2709
2200
 
@@ -2712,13 +2203,13 @@ class NodeGroup {
2712
2203
 
2713
2204
  // Event attribute value
2714
2205
  if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
2715
- let root = this.manager.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
2716
- path.applyEventAttrib(node2, expr, root);
2206
+ let root = this.manager?.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
2207
+ path.applyEventAttrib(el, expr, root);
2717
2208
  }
2718
2209
 
2719
2210
  // Regular attribute value.
2720
- else
2721
- exprIndex = path.applyValueAttrib(node2, exprs, exprIndex);
2211
+ else // One node value may have multiple expressions. Here we apply them all at once.
2212
+ exprIndex = path.applyValueAttrib(el, exprs, exprIndex);
2722
2213
  }
2723
2214
 
2724
2215
  lastNode = path.nodeMarker;
@@ -2747,6 +2238,10 @@ class NodeGroup {
2747
2238
  /*#IFDEV*/this.verify();/*#ENDIF*/
2748
2239
  }
2749
2240
 
2241
+ applyExpr(path, expr) {
2242
+ // TODO: Use this if I can figure out how to adapt applyValueAttrib() to it.
2243
+ }
2244
+
2750
2245
  /**
2751
2246
  * Insert/replace the nodes created by a single expression.
2752
2247
  * Called by applyExprs()
@@ -2768,7 +2263,7 @@ class NodeGroup {
2768
2263
  //for (let ng of path.nodeGroups) // TODO: Is this necessary?
2769
2264
  // ng.parentPath = null;
2770
2265
  path.nodeGroups = [];
2771
- this.applyOneExpr(expr, path, newNodes, secondPass);
2266
+ path.apply(expr, newNodes, secondPass);
2772
2267
  this.existingTextNodes = null;
2773
2268
 
2774
2269
  // TODO: Create an array of old vs Nodes and NodeGroups together.
@@ -2858,117 +2353,32 @@ class NodeGroup {
2858
2353
  }
2859
2354
  }
2860
2355
 
2861
- // TODO: Move to ExprPath?
2862
- applyOneExpr(expr, path, newNodes, secondPass) {
2863
-
2864
- if (expr instanceof Template) {
2865
- expr.parentPath = path;
2866
- expr.nodegroup = this;
2867
-
2868
- //if (window.debug && expr.exprs[0] === 'Banana' && path.nodeGroups.length === 0)
2869
- //if (window.debug && expr.exprs[0] === 'Banana')
2870
- // debugger;
2871
-
2872
- let ng = this.manager.getNodeGroup(expr, true);
2873
-
2874
-
2875
- if (ng) {
2876
- //#IFDEV
2877
- // Make sure the nodeCache of the ExprPath we took it from is sitll valid.
2878
- if (ng.parentPath)
2879
- ng.parentPath.verify();
2880
- //#ENDIF
2881
-
2882
-
2883
- // TODO: Track ranges of changed nodes and only pass those to udomdiff?
2884
- // But will that break the swap benchmark?
2885
- newNodes.push(...ng.getNodes());
2886
- path.nodeGroups.push(ng);
2887
- }
2888
-
2889
- // If expression, evaluate later to find partial match.
2890
- else {
2891
- secondPass.push([newNodes.length, path.nodeGroups.length]);
2892
- newNodes.push(expr);
2893
- path.nodeGroups.push(null); // placeholder
2894
- }
2895
- }
2896
-
2897
- // Node created by an expression.
2898
- else if (expr instanceof Node) {
2899
-
2900
- // DocumentFragment created by an expression.
2901
- if (expr instanceof DocumentFragment)
2902
- newNodes.push(...expr.childNodes);
2903
- else
2904
- newNodes.push(expr);
2905
- }
2906
-
2907
- else if (Array.isArray(expr))
2908
- for (let subExpr of expr)
2909
- this.applyOneExpr(subExpr, path, newNodes, secondPass);
2910
-
2911
- else if (typeof expr === 'function') {
2912
- expr = watchFunction(expr, this.manager);
2913
-
2914
- this.applyOneExpr(expr, path, newNodes, secondPass);
2915
- }
2916
-
2917
- // Text
2918
- else {
2919
- // Convert falsy values (but not 0) to empty string.
2920
- // Convert numbers to string so they compare the same.
2921
- let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
2922
-
2923
- // Fast path for updating the text of a single text node.
2924
- let first = path.nodeBefore.nextSibling;
2925
- if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
2926
- if (first.textContent !== text)
2927
- first.textContent = text;
2928
-
2929
- newNodes.push(first);
2930
- }
2931
-
2932
- else {
2933
- // TODO: Optimize this into a Set or Map or something?
2934
- if (!this.existingTextNodes)
2935
- this.existingTextNodes = path.getNodes().filter(n => n.nodeType === 3);
2936
-
2937
- let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
2938
- if (idx !== -1)
2939
- newNodes.push(...this.existingTextNodes.splice(idx, 1));
2940
- else
2941
- newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
2942
- }
2943
- }
2944
- }
2945
-
2946
2356
  /**
2947
2357
  * Create a nested RedComponent or call render with the new props.
2948
2358
  * @param el {Solarite:HTMLElement}
2949
2359
  * @param props {Object} */
2950
2360
  applyComponentExprs(el, props) {
2951
-
2361
+
2952
2362
  // TODO: Does a hash of this already exist somewhere?
2953
2363
  // Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
2954
2364
  // then we could re-use the hash and logic from NodeManager?
2955
2365
  let newHash = getObjectHash(props);
2956
-
2957
- let isPreHtmlElement = el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER');
2366
+
2367
+ let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
2958
2368
  let isPreIsElement = el.hasAttribute('_is');
2959
-
2960
-
2369
+
2370
+
2961
2371
  // Instantiate a placeholder.
2962
2372
  if (isPreHtmlElement || isPreIsElement)
2963
2373
  el = this.createNewComponent(el, isPreHtmlElement, props);
2964
-
2374
+
2965
2375
  // Update params of placeholder.
2966
2376
  else if (el.render) {
2967
2377
  let oldHash = componentHash.get(el);
2968
2378
  if (oldHash !== newHash)
2969
2379
  el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
2970
2380
  }
2971
-
2381
+
2972
2382
  componentHash.set(el, newHash);
2973
2383
  }
2974
2384
 
@@ -2987,8 +2397,8 @@ class NodeGroup {
2987
2397
  isPreHtmlElement = !el.hasAttribute('_is');
2988
2398
 
2989
2399
  let tagName = (isPreHtmlElement
2990
- ? el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER')
2991
- ? el.tagName.slice(0, -25)
2400
+ ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
2401
+ ? el.tagName.slice(0, -21)
2992
2402
  : el.tagName
2993
2403
  : el.getAttribute('is')).toLowerCase();
2994
2404
 
@@ -3108,6 +2518,17 @@ class NodeGroup {
3108
2518
  }
3109
2519
 
3110
2520
 
2521
+
2522
+ updateStyles() {
2523
+ if (this.styles)
2524
+ for (let [style, oldText] of this.styles) {
2525
+ let newText = style.textContent;
2526
+ if (oldText !== newText)
2527
+ Util.bindStyles(style, this.manager.rootEl);
2528
+ }
2529
+ }
2530
+
2531
+
3111
2532
  //#IFDEV
3112
2533
  /**
3113
2534
  * @deprecated
@@ -3179,12 +2600,21 @@ class NodeGroup {
3179
2600
 
3180
2601
  let componentHash = new WeakMap();
3181
2602
 
2603
+ /**
2604
+ * Tools for watch variables and performing precise renders.
2605
+ */
2606
+
2607
+ function serializePath(path) {
2608
+ // Convert any array indices to strings, so serialized comparisons work.
2609
+ return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
2610
+
2611
+ }
2612
+
3182
2613
  /**
3183
2614
  * @typedef {Object} RenderOptions
3184
2615
  * @property {boolean=} styles - Indicates whether the Courage component is present.
3185
2616
  * @property {boolean=} scripts - Indicates whether the Power component is present.
3186
- * @property {boolean=} ids
3187
- *
2617
+ * @property {boolean=} ids *
3188
2618
  * @property {?boolean} render
3189
2619
  * Used only when options are given to a class super constructor inheriting from Solarite.
3190
2620
  * True to call render() immediately in super constructor.
@@ -3239,6 +2669,7 @@ class NodeGroupManager {
3239
2669
  * @param rootEl {HTMLElement|DocumentFragment} If not specified, the first element of the html will be the rootEl. */
3240
2670
  constructor(rootEl=null) {
3241
2671
  this.rootEl = rootEl;
2672
+
3242
2673
  /*
3243
2674
  //#IFDEV
3244
2675
 
@@ -3280,126 +2711,6 @@ class NodeGroupManager {
3280
2711
  */
3281
2712
  }
3282
2713
 
3283
- /**
3284
- * Render the main template, which may indirectly call renderTemplate() to create children.
3285
- * @param template {Template}
3286
- * @param options {RenderOptions}
3287
- * @return {?DocumentFragment} */
3288
- render(template, options={}) {
3289
- this.mutationWatcherEnabled = false;
3290
- this.options = options;
3291
- this.clearSubscribers = false;
3292
-
3293
- //#IFDEV
3294
- this.modifications = {
3295
- created: [],
3296
- updated: [],
3297
- moved: [],
3298
- deleted: []
3299
- };
3300
- //#ENDIF
3301
-
3302
- if (!template && template !== '') {
3303
- this.rootEl.outerHTML = '';
3304
- this.mutationWatcherEnabled = true;
3305
- return null;
3306
- }
3307
-
3308
- // Fast path for empty component.
3309
- if (template.html?.length === 1 && !template.html[0]) {
3310
- this.rootEl.innerHTML = '';
3311
- }
3312
- else {
3313
-
3314
- // Find or create a NodeGroup for the template.
3315
- // This updates all nodes from the template.
3316
- let close;
3317
- let exact = this.getNodeGroup(template, true);
3318
- if (!exact) {
3319
- close = this.getNodeGroup(template, false);
3320
- }
3321
-
3322
-
3323
- let firstTime = !this.rootNg;
3324
- this.rootNg = exact || close;
3325
-
3326
- // Reparent NodeGroup
3327
- // TODO: Move this to NodeGroup?
3328
- let parent = this.rootNg.getParentNode();
3329
- if (!this.rootEl)
3330
- this.rootEl = parent;
3331
-
3332
- // If this is the first time rendering this element.
3333
- else if (firstTime) {
3334
-
3335
- // Save slot children
3336
- let fragment;
3337
- if (this.rootEl.childNodes.length) {
3338
- fragment = document.createDocumentFragment();
3339
- fragment.append(...this.rootEl.childNodes);
3340
- }
3341
-
3342
- // Add rendered elements.
3343
- if (parent instanceof DocumentFragment)
3344
- this.rootEl.append(parent);
3345
- else if (parent)
3346
- this.rootEl.append(...parent.childNodes);
3347
-
3348
- // Apply slot children
3349
- if (fragment) {
3350
- for (let slot of this.rootEl.querySelectorAll('slot[name]')) {
3351
- let name = slot.getAttribute('name');
3352
- if (name)
3353
- slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
3354
- }
3355
- let unamedSlot = this.rootEl.querySelector('slot:not([name])');
3356
- if (unamedSlot)
3357
- unamedSlot.append(fragment);
3358
- }
3359
-
3360
- }
3361
-
3362
- // this.rootNg was rendered as childrenOnly=true
3363
- // Apply attributes from a root element to the real root element.
3364
- let ng = this.rootNg;
3365
- if (ng.pseudoRoot && ng.pseudoRoot !== this.rootEl) {
3366
- /*#IFDEV*/assert(this.rootEl);/*#ENDIF*/
3367
-
3368
- // Remove old attributes
3369
- // for (let attrib of this.rootEl.attributes)
3370
- // if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
3371
- // this.rootEl.removeAttribute(attrib.name)
3372
-
3373
- // Add/set new attributes
3374
- if (firstTime)
3375
- for (let attrib of ng.pseudoRoot.attributes)
3376
- if (!this.rootEl.hasAttribute(attrib.name))
3377
- this.rootEl.setAttribute(attrib.name, attrib.value);
3378
-
3379
- // ng.startNode = ng.endNode = this.rootEl;
3380
- // ng.nodesCache = [ng.startNode]
3381
- // for (let path of ng.paths) {
3382
- // if (path.nodeMarker === ng.rootEl)
3383
- // path.nodeMarker = this.rootEl;
3384
- // path.nodesCache = null;
3385
- // /*#IFDEV*/assert(path.nodeBefore !== ng.rootEl)/*#ENDIF*/
3386
- // }
3387
- //
3388
- // ng.rootEl = this.rootEl;
3389
- }
3390
-
3391
- /*#IFDEV*/this.rootNg.verify();/*#ENDIF*/
3392
- this.reset();
3393
- /*#IFDEV*/this.rootNg.verify();/*#ENDIF*/
3394
- }
3395
-
3396
- this.mutationWatcherEnabled = true;
3397
- return this.rootEl;
3398
- //#IFDEV
3399
- //return this.modifications;
3400
- //#ENDIF
3401
- }
3402
-
3403
2714
 
3404
2715
  /**
3405
2716
  *
@@ -3498,6 +2809,8 @@ class NodeGroupManager {
3498
2809
  // But it can still be a close match, so we don't use this code.
3499
2810
  success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
3500
2811
  /*#IFDEV*/assert(success);/*#ENDIF*/
2812
+
2813
+ this.nodeGroupsInUse.push(ng2);
3501
2814
  }
3502
2815
  }
3503
2816
  }
@@ -3528,7 +2841,7 @@ class NodeGroupManager {
3528
2841
  * but don't reparent it if it's somewhere else.
3529
2842
  * @param template {Template}
3530
2843
  * @param exact {?boolean}
3531
- * @param createForWatch
2844
+ * @param createForWatch Deprecated.
3532
2845
  * @return {?NodeGroup} */
3533
2846
  getNodeGroup(template, exact=null, createForWatch=false) {
3534
2847
 
@@ -3582,7 +2895,7 @@ class NodeGroupManager {
3582
2895
  // Perhaps also result could cache its last exprKey and then we'd use only one map?
3583
2896
  ng.exactKey = exactKey;
3584
2897
  ng.closeKey = closeKey;
3585
- if (createForWatch) // TODO: Have this path be a separate function?
2898
+ if (createForWatch)
3586
2899
  this.nodeGroupsAvailable.add(ng.exactKey, ng);
3587
2900
  else
3588
2901
  this.nodeGroupsInUse.push(ng);
@@ -3625,7 +2938,10 @@ class NodeGroupManager {
3625
2938
  //pathToLoopInfo = new MultiValueMap(); // uses a Set() for each value.
3626
2939
  clearSubscribers = false;
3627
2940
 
2941
+ //#IFDEV
2942
+
3628
2943
  /**
2944
+ * @deprecated - part of watch.js (Watch v1)
3629
2945
  * One path may be used to loop in more than one place, so we use this to get every anchor from each loop.
3630
2946
  * @param path {Array}
3631
2947
  * @return {LoopInfo[]} A function that gets the loop anchor NodeGroup */
@@ -3634,6 +2950,8 @@ class NodeGroupManager {
3634
2950
  return [...this.pathToLoopInfo.getAll(serializedArrayPath)]; // This is set inside forEach()
3635
2951
  }
3636
2952
 
2953
+ //#ENDIF
2954
+
3637
2955
 
3638
2956
  /**
3639
2957
  * @deprecated
@@ -3660,7 +2978,10 @@ class NodeGroupManager {
3660
2978
  * Get the NodeGroupManager for a Web Component.
3661
2979
  * @param rootEl {Solarite|HTMLElement}
3662
2980
  * @return {NodeGroupManager} */
3663
- static get(rootEl) {
2981
+ static get(rootEl=null) {
2982
+ if (!rootEl)
2983
+ return new NodeGroupManager();
2984
+
3664
2985
  let ngm = nodeGroupManagers.get(rootEl);
3665
2986
  if (!ngm) {
3666
2987
  ngm = new NodeGroupManager(rootEl);
@@ -3723,16 +3044,7 @@ NodeGroupManager.pendingChildren = [];
3723
3044
  /**
3724
3045
  * Each Element that has Expr children has an associated NodeGroupManager here.
3725
3046
  * @type {WeakMap<HTMLElement, NodeGroupManager>} */
3726
- let nodeGroupManagers = new WeakMap();
3727
-
3728
-
3729
-
3730
- class LoopInfo {
3731
- constructor(loopTemplate, itemTransformer) {
3732
- this.template = loopTemplate;
3733
- this.itemTransformer = itemTransformer;
3734
- }
3735
- }
3047
+ let nodeGroupManagers = new WeakMap();
3736
3048
 
3737
3049
  /**
3738
3050
  * Convert strings to HTMLNodes.
@@ -3757,10 +3069,10 @@ class LoopInfo {
3757
3069
  * 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3758
3070
  * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which includes properly handling nested components and r`` sub-expressions.
3759
3071
  * 8. r(template) // Render Template created by #1.
3760
- * 9. r(() => r`<b>Hello</b>`); // Create dynamic element that has a render() function.
3072
+ * 9. r(() => r`<b>Hello</b>`, {...}); // Create dynamic element that has a render() function.
3761
3073
  *
3762
3074
  * @param htmlStrings {?HTMLElement|string|string[]|function():Template}
3763
- * @param exprs {*[]|string|Template}
3075
+ * @param exprs {*[]|string|Template|Object}
3764
3076
  * @return {Node|HTMLElement|Template} */
3765
3077
  function r(htmlStrings=undefined, ...exprs) {
3766
3078
 
@@ -3774,9 +3086,8 @@ function r(htmlStrings=undefined, ...exprs) {
3774
3086
 
3775
3087
  // 2. Render template created by #4 to element.
3776
3088
  if (exprs[0] instanceof Template) {
3777
- let ngm = NodeGroupManager.get(parent);
3778
3089
  let options = exprs[1];
3779
- ngm.render(template, options);
3090
+ template.render(parent, options);
3780
3091
 
3781
3092
  // Append on the first go.
3782
3093
  if (!parent.childNodes.length && this) {
@@ -3795,15 +3106,16 @@ function r(htmlStrings=undefined, ...exprs) {
3795
3106
  return (htmlStrings, ...exprs) => {
3796
3107
  rendered.add(parent);
3797
3108
  let template = r(htmlStrings, ...exprs);
3798
- let ngm = NodeGroupManager.get(parent);
3799
- return ngm.render(template, options);
3109
+ return template.render(parent, options);
3800
3110
  }
3801
3111
  }
3802
3112
 
3803
3113
  // null for expr[0], remove whole element.
3114
+ // This path never happens?
3804
3115
  else {
3805
- let ngm = NodeGroupManager.get(parent);
3806
- ngm.render(null, exprs[1]);
3116
+ throw new Error('unsupported');
3117
+ //let ngm = NodeGroupManager.get(parent);
3118
+ //ngm.render(null, exprs[1])
3807
3119
  }
3808
3120
  }
3809
3121
 
@@ -3831,16 +3143,16 @@ function r(htmlStrings=undefined, ...exprs) {
3831
3143
  return (htmlStrings, ...exprs) => {
3832
3144
  //rendered.add(parent)
3833
3145
  let template = r(htmlStrings, ...exprs);
3834
- return template.toNode();
3146
+ return template.render();
3835
3147
  }
3836
3148
  }
3837
3149
 
3838
3150
  // 8.
3839
3151
  else if (htmlStrings instanceof Template) {
3840
- let ngm = new NodeGroupManager();
3841
- return ngm.render(htmlStrings);
3152
+ return htmlStrings.render();
3842
3153
  }
3843
3154
 
3155
+
3844
3156
  // 9. Create dynamic element with render() function.
3845
3157
  else if (typeof htmlStrings === 'function') {
3846
3158
  let getTemplate = htmlStrings;
@@ -3849,17 +3161,27 @@ function r(htmlStrings=undefined, ...exprs) {
3849
3161
  if (typeof template === 'string')
3850
3162
  throw new Error(`Please add the "r" prefix before the string "${template}"`)
3851
3163
 
3852
- let ngm = new NodeGroupManager();
3853
3164
  template.replaceMode = true;
3854
- let el = ngm.render(template);
3165
+ let el = template.render();
3855
3166
 
3167
+ // Create the render() function from the function we were given.
3856
3168
  el.render = (function() {
3857
3169
  template = getTemplate();
3858
- ngm.render(template);
3170
+ template.render(el);
3859
3171
  }).bind(el);
3860
3172
 
3173
+
3174
+ // The second argument was an object of additional properties to add.
3175
+ let props = exprs[0];
3176
+ for (let name in props)
3177
+ if (typeof props[name] === 'function')
3178
+ el[name] = props[name].bind(el);
3179
+ else
3180
+ el[name] = props[name];
3181
+
3861
3182
  return el;
3862
3183
  }
3184
+
3863
3185
  else
3864
3186
  throw new Error('Unsupported arguments.')
3865
3187
  }
@@ -3874,6 +3196,10 @@ function r(htmlStrings=undefined, ...exprs) {
3874
3196
  * @type {WeakSet<HTMLElement>} */
3875
3197
  let rendered = new WeakSet();
3876
3198
 
3199
+ //import {watchGet, watchSet} from "./watch.js";
3200
+
3201
+
3202
+
3877
3203
  function defineClass(Class, tagName, extendsTag) {
3878
3204
  if (!customElements.getName(Class)) { // If not previously defined.
3879
3205
  tagName = tagName || camelToDashes(Class.name);
@@ -3904,9 +3230,15 @@ let connected = new WeakSet();
3904
3230
  * 2. Calls render() when added to the DOM, if it hasn't been called already.
3905
3231
  * 3. Child elements are added before constructor is called. But they're also passed to the constructor.
3906
3232
  * 4. We can use this.html = r`...` to set html.
3907
- * 5. We have the onConnect, onFirstConnect, and onDisconnect methods. These could be standalone though.
3233
+ * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
3234
+ * Can't figure out how to have these work standalone though, and still be synchronous.
3908
3235
  * 6. Can we extend from other element types like TR?
3909
3236
  *
3237
+ * Advantages to inheriting from HTMLElement
3238
+ * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
3239
+ * 2. We can inherit from things like HTMLTableRowElement directly.
3240
+ * 3. There's less magic, since everyone is familiar with defining custom elements.
3241
+ *
3910
3242
  * @param extendsTag {?string}
3911
3243
  * @return {Class} */
3912
3244
  function createSolarite(extendsTag=null) {
@@ -3970,7 +3302,7 @@ function createSolarite(extendsTag=null) {
3970
3302
  if (ch)
3971
3303
  (this.querySelector('slot') || this).append(...ch);
3972
3304
 
3973
-
3305
+ /** @deprecated */
3974
3306
  Object.defineProperty(this, 'html', {
3975
3307
  set(html) {
3976
3308
  rendered.add(this);
@@ -4020,7 +3352,9 @@ function createSolarite(extendsTag=null) {
4020
3352
  defineClass(this, tagName, extendsTag);
4021
3353
  }
4022
3354
 
3355
+ //#IFDEV
4023
3356
 
3357
+ /** @deprecated */
4024
3358
  renderWatched() {
4025
3359
  let ngm = NodeGroupManager.get(this);
4026
3360
 
@@ -4094,8 +3428,7 @@ function createSolarite(extendsTag=null) {
4094
3428
  // Create new NodeGroup
4095
3429
  let ng = ngm.getNodeGroup(template, false, true);
4096
3430
  ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
4097
- /*#IFDEV*/
4098
- assert(ng.parentPath);/*#ENDIF*/
3431
+
4099
3432
  for (let node of ng.getNodes())
4100
3433
  beforeNode.parentNode.insertBefore(node, beforeNode);
4101
3434
 
@@ -4125,8 +3458,9 @@ function createSolarite(extendsTag=null) {
4125
3458
  /**
4126
3459
  * @deprecated Use the getArg() function instead. */
4127
3460
  getArg(name, val=null, type=ArgType.String) {
4128
- return getArg(this, name, val, type);
3461
+ throw new Error('deprecated');
4129
3462
  }
3463
+ //#ENDIF
4130
3464
  }
4131
3465
  }
4132
3466
 
@@ -4138,6 +3472,10 @@ let Solarite = new Proxy(createSolarite(), {
4138
3472
  return createSolarite(...args)
4139
3473
  }
4140
3474
  });
4141
- // unfinished
3475
+
3476
+
3477
+ //Experimental:
3478
+ //export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
3479
+ //export {watch} from './watch2.js'; // unfinished
4142
3480
 
4143
- export { ArgType, Solarite, forEach, getArg, r, watch, watchGet, watchSet };
3481
+ export { ArgType, Solarite, Template, getArg, r };