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.
package/dist/Solarite.js CHANGED
@@ -81,8 +81,6 @@ var Util$1 = {
81
81
 
82
82
  };
83
83
 
84
-
85
-
86
84
  /**
87
85
  * Follow a path into an object.
88
86
  * @param obj {object}
@@ -180,19 +178,19 @@ function getArg(el, name, val=null, type=ArgType.String) {
180
178
  case ArgType.Float:
181
179
  return parseFloat(val);
182
180
  case ArgType.String:
183
- return val || '';
181
+ return [undefined, null, false].includes(val) ? '' : val+'';
184
182
  case ArgType.JSON:
185
- try {
186
- return JSON.parse(val);
187
- } catch (e) {
188
- return val;
189
- }
190
183
  case ArgType.Eval:
191
- try {
192
- return eval(`(${val})`);
193
- } catch (e) {
194
- return val;
195
- }
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;
196
194
  default:
197
195
  return val;
198
196
  }
@@ -336,7 +334,11 @@ class MultiValueMap {
336
334
  return this.data[key] || [];
337
335
  }
338
336
 
339
- // Remove one value from a key, and return it
337
+ /**
338
+ * Remove one value from a key, and return it.
339
+ * @param key {string}
340
+ * @param val If specified, make sure we delete this specific value, if a key exists more than once.
341
+ * @returns {*} */
340
342
  delete(key, val=undefined) {
341
343
  // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
342
344
  // debugger;
@@ -381,6 +383,8 @@ class MultiValueMap {
381
383
  }
382
384
  }
383
385
 
386
+
387
+
384
388
  let Util = {
385
389
 
386
390
  bindStyles(style, root) {
@@ -542,6 +546,191 @@ let state = {...defaultState};
542
546
  // For debugging only
543
547
 
544
548
 
549
+ /**
550
+ * The html strings and evaluated expressions from an html tagged template.
551
+ * A unique Template is created for each item in a loop.
552
+ * Although the reference to the html strings is shared among templates. */
553
+ class Template {
554
+
555
+ /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
556
+ exprs = []
557
+
558
+ /** @type {string[]} */
559
+ html = [];
560
+
561
+ /**
562
+ * If true, use this template to replace an existing element, instead of appending children to it.
563
+ * @type {?boolean} */
564
+ replaceMode;
565
+
566
+ /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
567
+ hashedFields;
568
+
569
+ /**
570
+ * @deprecated
571
+ * @type {ExprPath} Used with forEach() from watch.js
572
+ * Set in ExprPath.apply() */
573
+ parentPath;
574
+
575
+ /** @type {NodeGroup} */
576
+ nodeGroup;
577
+
578
+ /**
579
+ * @type {string[][]} */
580
+ paths = [];
581
+
582
+ /**
583
+ *
584
+ * @param htmlStrings {string[]}
585
+ * @param exprs {*[]} */
586
+ constructor(htmlStrings, exprs) {
587
+ this.html = htmlStrings;
588
+ this.exprs = exprs;
589
+
590
+ //this.trace = new Error().stack.split(/\n/g)
591
+
592
+ // Multiple templates can share the same htmlStrings array.
593
+ //this.hashedFields = [getObjectId(htmlStrings), exprs]
594
+
595
+
596
+ }
597
+
598
+ /**
599
+ * Called by JSON.serialize when it encounters a Template.
600
+ * This prevents the hashed version from being too large. */
601
+ toJSON() {
602
+ if (!this.hashedFields)
603
+ this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
604
+
605
+ return this.hashedFields
606
+ }
607
+
608
+ /**
609
+ * Render the main template, which may indirectly call renderTemplate() to create children.
610
+ * @param el {HTMLElement}
611
+ * @param options {RenderOptions}
612
+ * @return {?DocumentFragment|HTMLElement} */
613
+ render(el=null, options={}) {
614
+
615
+ let ng;
616
+ if (!el) {
617
+ ng = new NodeGroup(this);
618
+ el = ng.getParentNode();
619
+ }
620
+
621
+ let ngm = NodeGroupManager.get(el);
622
+ if (ng)
623
+ ng.manager = ngm;
624
+
625
+
626
+
627
+ ngm.options = options;
628
+ ngm.clearSubscribers = false; // Used for deprecated watch() path?
629
+ ngm.mutationWatcherEnabled = false;
630
+
631
+ // Fast path for empty component.
632
+ if (this.html?.length === 1 && !this.html[0]) {
633
+ el.innerHTML = '';
634
+ }
635
+ else {
636
+
637
+ // Find or create a NodeGroup for the template.
638
+ // This updates all nodes from the template.
639
+ let close;
640
+ let exact = ngm.getNodeGroup(this, true);
641
+ if (!exact) {
642
+ close = ngm.getNodeGroup(this, false);
643
+ }
644
+
645
+ let firstTime = !ngm.rootNg;
646
+ ngm.rootNg = exact || close;
647
+
648
+ // Reparent NodeGroup
649
+ // TODO: Move this to NodeGroup?
650
+ let parent = ngm.rootNg.getParentNode();
651
+
652
+
653
+ // If this is the first time rendering this element.
654
+ if (firstTime) {
655
+
656
+ // Save slot children
657
+ let fragment;
658
+ if (el.childNodes.length) {
659
+ fragment = document.createDocumentFragment();
660
+ fragment.append(...el.childNodes);
661
+ }
662
+
663
+ // Add rendered elements.
664
+ if (parent instanceof DocumentFragment)
665
+ el.append(parent);
666
+ else if (parent)
667
+ el.append(...parent.childNodes);
668
+
669
+ // Apply slot children
670
+ if (fragment) {
671
+ for (let slot of el.querySelectorAll('slot[name]')) {
672
+ let name = slot.getAttribute('name');
673
+ if (name)
674
+ slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
675
+ }
676
+ let unamedSlot = el.querySelector('slot:not([name])');
677
+ if (unamedSlot)
678
+ unamedSlot.append(fragment);
679
+ }
680
+ }
681
+
682
+ ngm.rootEl = el;
683
+
684
+ // this.rootNg was rendered as childrenOnly=true
685
+ // Apply attributes from a root element to the real root element.
686
+ let ng = ngm.rootNg;
687
+ if (ng.pseudoRoot && ng.pseudoRoot !== el) {
688
+
689
+
690
+ // Remove old attributes
691
+ // for (let attrib of this.rootEl.attributes)
692
+ // if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
693
+ // this.rootEl.removeAttribute(attrib.name)
694
+
695
+ // Add/set new attributes
696
+ if (firstTime)
697
+ for (let attrib of ng.pseudoRoot.attributes)
698
+ if (!el.hasAttribute(attrib.name))
699
+ el.setAttribute(attrib.name, attrib.value);
700
+
701
+ // ng.startNode = ng.endNode = this.rootEl;
702
+ // ng.nodesCache = [ng.startNode]
703
+ // for (let path of ng.paths) {
704
+ // if (path.nodeMarker === ng.rootEl)
705
+ // path.nodeMarker = this.rootEl;
706
+ // path.nodesCache = null;
707
+ //
708
+ // }
709
+ //
710
+ // ng.rootEl = this.rootEl;
711
+ }
712
+
713
+
714
+ ngm.reset(); // Mark all NodeGroups as available, for next render.
715
+
716
+
717
+ window.ngm = ngm;
718
+ }
719
+
720
+ ngm.mutationWatcherEnabled = true;
721
+ return el;
722
+
723
+ }
724
+
725
+
726
+ getCloseKey() {
727
+ // Use the joined html when debugging?
728
+ //return '@'+this.html.join('|')
729
+
730
+ return '@'+this.hashedFields[0];
731
+ }
732
+ }
733
+
545
734
  /**
546
735
  * Path to where an expression should be evaluated within a Shell.
547
736
  * Path is only valid until the expressions before it are evaluated.
@@ -636,6 +825,86 @@ class ExprPath {
636
825
  this.attrNames = new Set();
637
826
  }
638
827
 
828
+ /**
829
+ *
830
+ * @param expr {Template|Node|Array|function|*}
831
+ * @param newNodes {(Node|Template)[]}
832
+ * @param secondPass {Array} Locations within newNodes to evaluate later. */
833
+ apply(expr, newNodes, secondPass) {
834
+
835
+ if (expr instanceof Template) {
836
+ expr.nodegroup = this.parentNg; // All tests pass w/o this.
837
+
838
+ let ng = this.parentNg.manager.getNodeGroup(expr, true);
839
+
840
+
841
+ if (ng) {
842
+
843
+
844
+
845
+ // TODO: Track ranges of changed nodes and only pass those to udomdiff?
846
+ // But will that break the swap benchmark?
847
+ newNodes.push(...ng.getNodes());
848
+ this.nodeGroups.push(ng);
849
+ }
850
+
851
+ // If expression, evaluate later to find partial match.
852
+ else {
853
+ secondPass.push([newNodes.length, this.nodeGroups.length]);
854
+ newNodes.push(expr);
855
+ this.nodeGroups.push(null); // placeholder
856
+ }
857
+ }
858
+
859
+ // Node created by an expression.
860
+ else if (expr instanceof Node) {
861
+
862
+ // DocumentFragment created by an expression.
863
+ if (expr instanceof DocumentFragment)
864
+ newNodes.push(...expr.childNodes);
865
+ else
866
+ newNodes.push(expr);
867
+ }
868
+
869
+ else if (Array.isArray(expr))
870
+ for (let subExpr of expr)
871
+ this.apply(subExpr, newNodes, secondPass);
872
+
873
+ else if (typeof expr === 'function') {
874
+ let result = expr();
875
+
876
+ this.apply(result, newNodes, secondPass);
877
+ }
878
+
879
+ // Text
880
+ else {
881
+ // Convert falsy values (but not 0) to empty string.
882
+ // Convert numbers to string so they compare the same.
883
+ let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
884
+
885
+ // Fast path for updating the text of a single text node.
886
+ let first = this.nodeBefore.nextSibling;
887
+ if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
888
+ if (first.textContent !== text)
889
+ first.textContent = text;
890
+
891
+ newNodes.push(first);
892
+ }
893
+
894
+ else {
895
+ // TODO: Optimize this into a Set or Map or something?
896
+ if (!this.existingTextNodes)
897
+ this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
898
+
899
+ let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
900
+ if (idx !== -1)
901
+ newNodes.push(...this.existingTextNodes.splice(idx, 1));
902
+ else
903
+ newNodes.push(this.parentNode.ownerDocument.createTextNode(text));
904
+ }
905
+ }
906
+ }
907
+
639
908
  applyMultipleAttribs(node, expr) {
640
909
 
641
910
 
@@ -696,7 +965,6 @@ class ExprPath {
696
965
  func = setValue;
697
966
  args = [expr[0], expr.slice(1), node];
698
967
  node.value = delve(expr[0], expr.slice(1));
699
- //debugger;
700
968
  }
701
969
  }
702
970
  else
@@ -814,8 +1082,7 @@ class ExprPath {
814
1082
  * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
815
1083
  * share the same DOM parent node.
816
1084
  *
817
- * TODO: Is recursive clearing ever necessary?
818
- */
1085
+ * TODO: Is recursive clearing ever necessary? */
819
1086
  clearNodesCache() {
820
1087
  let path = this;
821
1088
 
@@ -833,7 +1100,7 @@ class ExprPath {
833
1100
 
834
1101
  // Clear cache of child ExprPaths that have the same parentNode
835
1102
  for (let ng of path.nodeGroups) {
836
- if (ng) // Can be null from applyOneExpr()'s push(null) call.
1103
+ if (ng) // Can be null from apply()'s push(null) call.
837
1104
  for (let path2 of ng.paths) {
838
1105
  if (path2.type === PathType.Content && path2.parentNode === parentNode) {
839
1106
  path2.nodesCache = null;
@@ -1039,7 +1306,7 @@ class Shell {
1039
1306
 
1040
1307
  // Swap out Embedded Solarite Components with ${} attributes.
1041
1308
  // Later, NodeGroup.render() will search for these and replace them with the real components.
1042
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1309
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1043
1310
  if (context === htmlContext.Attribute) {
1044
1311
 
1045
1312
  let lastIndex, lastMatch;
@@ -1049,7 +1316,7 @@ class Shell {
1049
1316
  });
1050
1317
 
1051
1318
  if (lastMatch) {
1052
- let newTagName = lastMatch + '-redcomponent-placeholder';
1319
+ let newTagName = lastMatch + '-solarite-placeholder';
1053
1320
  lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
1054
1321
  componentNames[lastMatch] = newTagName;
1055
1322
  }
@@ -1066,9 +1333,9 @@ class Shell {
1066
1333
 
1067
1334
  // 2. Create elements from html with placeholders.
1068
1335
  let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
1069
- let joinedHtml = buffer.join('');
1070
-
1071
- // Replace '-redcomponent-placeholder' close tags.
1336
+ let joinedHtml = buffer.join('');
1337
+
1338
+ // Replace '-solarite-placeholder' close tags.
1072
1339
  // TODO: is there a better way? What if the close tag is inside a comment?
1073
1340
  for (let name in componentNames)
1074
1341
  joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
@@ -1091,7 +1358,7 @@ class Shell {
1091
1358
 
1092
1359
  // Replace attributes
1093
1360
  if (node.nodeType === 1) {
1094
- for (let attr of node.attributes) {
1361
+ for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
1095
1362
 
1096
1363
  // Whole attribute
1097
1364
  let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
@@ -1187,8 +1454,8 @@ class Shell {
1187
1454
  }
1188
1455
  toRemove.map(el => el.remove());
1189
1456
 
1190
- // Handle redcomponent-placeholder's.
1191
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1457
+ // Handle solarite-placeholder's.
1458
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1192
1459
  //if (componentNames.size)
1193
1460
  // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
1194
1461
 
@@ -1276,106 +1543,34 @@ class Shell {
1276
1543
  let shells = new WeakMap();
1277
1544
 
1278
1545
  /**
1279
- * The html strings and evaluated expressions from an html tagged template.
1280
- * A unique Template is created for each item in a loop.
1281
- * Although the reference to the html strings is shared among templates. */
1282
- class Template {
1546
+ * ISC License
1547
+ *
1548
+ * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
1549
+ *
1550
+ * Permission to use, copy, modify, and/or distribute this software for any
1551
+ * purpose with or without fee is hereby granted, provided that the above
1552
+ * copyright notice and this permission notice appear in all copies.
1553
+ *
1554
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1555
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1556
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1557
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1558
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
1559
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1560
+ * PERFORMANCE OF THIS SOFTWARE.
1561
+ */
1283
1562
 
1284
- /** @type {(Template|string|function)|(Template|string|function)[]} Evaulated expressions. */
1285
- exprs = []
1286
-
1287
- /** @type {string[]} */
1288
- html = [];
1289
-
1290
- /**
1291
- * If true, use this template to replace an existing element, instead of appending children to it.
1292
- * @type {?boolean} */
1293
- replaceMode;
1294
-
1295
- /** Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
1296
- hashedFields;
1297
-
1298
- /**
1299
- * @deprecated
1300
- * @type {ExprPath} Used with forEach() from watch.js
1301
- * Set in NodeGroup.applyOneExpr() */
1302
- parentPath;
1303
-
1304
- /** @type {NodeGroup} */
1305
- nodeGroup;
1306
-
1307
- /**
1308
- * @type {string[][]} */
1309
- paths = [];
1310
-
1311
- /**
1312
- *
1313
- * @param htmlStrings {string[]}
1314
- * @param exprs {*[]} */
1315
- constructor(htmlStrings, exprs) {
1316
- this.html = htmlStrings;
1317
- this.exprs = exprs;
1318
-
1319
- //this.trace = new Error().stack.split(/\n/g)
1320
-
1321
- // Multiple templates can share the same htmlStrings array.
1322
- //this.hashedFields = [getObjectId(htmlStrings), exprs]
1323
-
1324
-
1325
- }
1326
-
1327
- /**
1328
- * Called by JSON.serialize when it encounters a Template.
1329
- * This prevents the hashed version from being too large. */
1330
- toJSON() {
1331
- if (!this.hashedFields)
1332
- this.hashedFields = [getObjectId(this.html, 'Html'), this.exprs];
1333
-
1334
- return this.hashedFields
1335
- }
1336
-
1337
- toNode() {
1338
- let ngm = new NodeGroupManager();
1339
- return ngm.render(this);
1340
- }
1341
-
1342
- getCloseKey() {
1343
- // Use the joined html when debugging?
1344
- //return '@'+this.html.join('|')
1345
-
1346
- return '@'+this.hashedFields[0];
1347
- }
1348
- }
1349
-
1350
- /**
1351
- * ISC License
1352
- *
1353
- * Copyright (c) 2020, Andrea Giammarchi, @WebReflection
1354
- *
1355
- * Permission to use, copy, modify, and/or distribute this software for any
1356
- * purpose with or without fee is hereby granted, provided that the above
1357
- * copyright notice and this permission notice appear in all copies.
1358
- *
1359
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1360
- * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1361
- * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1362
- * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1363
- * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
1364
- * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1365
- * PERFORMANCE OF THIS SOFTWARE.
1366
- */
1367
-
1368
- /**
1369
- * @param {Node} parentNode The container where children live
1370
- * @param {Node[]} a The list of current/live children
1371
- * @param {Node[]} b The list of future children
1372
- * @param {(entry: Node, action: number) => Node} get
1373
- * The callback invoked per each entry related DOM operation.
1374
- * @param {Node} [before] The optional node used as anchor to insert before.
1375
- * @returns {Node[]} The same list of future children.
1376
- */
1377
- const udomdiff = (parentNode, a, b, before) => {
1378
-
1563
+ /**
1564
+ * @param {Node} parentNode The container where children live
1565
+ * @param {Node[]} a The list of current/live children
1566
+ * @param {Node[]} b The list of future children
1567
+ * @param {(entry: Node, action: number) => Node} get
1568
+ * The callback invoked per each entry related DOM operation.
1569
+ * @param {Node} [before] The optional node used as anchor to insert before.
1570
+ * @returns {Node[]} The same list of future children.
1571
+ */
1572
+ const udomdiff = (parentNode, a, b, before) => {
1573
+
1379
1574
 
1380
1575
  const bLength = b.length;
1381
1576
  let aEnd = a.length;
@@ -1534,723 +1729,8 @@ const udomdiff = (parentNode, a, b, before) => {
1534
1729
  }
1535
1730
  }
1536
1731
  }
1537
- return b;
1538
- };
1539
-
1540
- /**
1541
- * Tools for watch variables and performing precise renders.
1542
- */
1543
-
1544
-
1545
- /**
1546
- * Stores info how to transform a path to a template. */
1547
- class TransformerInfo {
1548
- constructor(path, transformer, hash) {
1549
- this.path = path;
1550
- this.transformer = transformer;
1551
- this.hash = hash;
1552
- }
1553
- }
1554
-
1555
-
1556
- /**
1557
- * Maps an object path to the function that converts it to a Template.
1558
- * Once it's convert to a template, we can get the hash of that Tempate.
1559
- * Then that hash tells us what NodeGroups are affected by the object.
1560
- *
1561
- * We store the function to get the Template, instead of the Template itself,
1562
- * so we can call that function again when the object has a new value.
1563
- * @type {MultiValueMap<Object, function(...Object):Template>} */
1564
- let pathToTransformer = new MultiValueMap(); // uses a Set() for each value.
1565
-
1566
- /**
1567
- *
1568
- * @param objectPaths {(*|function)[]}
1569
- * @returns {Template} */
1570
- function watchGet(...objectPaths) {
1571
-
1572
- /** @type {function} */
1573
- let transformer = objectPaths.at(-1);
1574
- let paths;
1575
- if (typeof transformer === 'function') {
1576
- paths = objectPaths.slice(0, -1);
1577
- }
1578
-
1579
- // No transformer provided, so we create our own.
1580
- else if (objectPaths.length === 1) {
1581
- paths = [objectPaths[0].slice(0, -1)];
1582
- let prop = objectPaths[0].at(-1);
1583
- transformer = (...args) => (args[0][prop]);
1584
- }
1585
-
1586
-
1587
- // Save arguments used to call the template, so we can call it again when those args have their values change.
1588
- let args = [];
1589
- for (let path of paths) {
1590
-
1591
-
1592
- let obj = delve(watchSet(path[0]), path.slice(1));
1593
- args.push(obj);
1594
- }
1595
-
1596
- let template = transformer(...args);
1597
-
1598
- // If the result isn't a Template, convert the function to return a Template that wraps the result.
1599
- // This way NodeGroupManager.findAndDelete() can find a NodeGroup that matches this Template's hash.
1600
- if (!(template instanceof Template)) {
1601
- let oldToTemplate = transformer;
1602
- transformer = function() {
1603
- return new Template(['', ''], [oldToTemplate(...arguments)]);
1604
- };
1605
- template = transformer(...args);
1606
- }
1607
-
1608
- // Map the object paths to the function that creates a template.
1609
- let hash = getObjectHash(template);
1610
- for (let path of paths) {
1611
- let serializedPath = serializePath(path);
1612
- pathToTransformer.add(serializedPath, new TransformerInfo(path, transformer, hash)); // Uses a Set() to ensure no duplicates.
1613
- }
1614
-
1615
- return template;
1616
- }
1617
-
1618
- //let proxyCache = new WeakMap();
1619
-
1620
-
1621
- /**
1622
- * Set the value of a variable in a way that's watched, so later when we call .renderWatched()
1623
- * We can find what NodeGroups to update.*/
1624
- function watchSet(obj) {
1625
- if (obj?.$isProxy===true)
1626
- return obj; // It's already a Proxy.
1627
-
1628
- // This cache doesn't make things faster.
1629
- // let result = proxyCache.get(obj);
1630
- // if (!result) {
1631
- // result = new Proxy(obj, new ProxyHandler(obj));
1632
- // proxyCache.set(obj, result);
1633
- // }
1634
- // return result;
1635
- return new Proxy(obj, new ProxyHandler$1(obj));
1636
- }
1637
-
1638
- /**
1639
- * Loop over each item and apply watchGet() to each item.
1640
- * @param arrayPath {*[]}
1641
- * @param callback {function(obj:Object, index:int):Template}
1642
- * @returns {Template} */
1643
- function forEach(arrayPath, callback) {
1644
- let array = delve(arrayPath[0], arrayPath.slice(1));
1645
-
1646
- // This is retrieved on the 'insert' path inside renderWatched()
1647
- let ngm = NodeGroupManager.get(arrayPath[0]);
1648
- if (ngm.clearSubscribers) {
1649
- ngm.clearSubscribers = false;
1650
- ngm.pathToLoopInfo = new MultiValueMap();
1651
- } // TODO: Move tis into NodeGroupMAnager.get() without breaking things?
1652
-
1653
-
1654
- let newItems = [...array.map((item, i) => {
1655
- // TODO: This needs to wrap callback so we can pass it the index also.
1656
- return watchGet([...arrayPath, i], callback); // calls callback(array[i], i)
1657
- })
1658
- ];
1659
-
1660
- // We return a template that wraps the array
1661
- // So that NodeGroup.applyOneExpr can set the ExprPath and nextSibling on the template.
1662
- // Then the 'insert' path in renderWatched() uses that data fora dding more nodes.
1663
- let result = new Template(['', ''], [newItems]);
1664
-
1665
-
1666
- // We get a unique hash for each foreach template because the [''] array is unique each time.
1667
- let loopInfo = new LoopInfo(result, callback);
1668
- ngm.pathToLoopInfo.add(serializePath(arrayPath), loopInfo);
1669
- return result;
1670
- }
1671
-
1672
- function serializePath(path) {
1673
- // Convert any array indices to strings, so serialized comparisons work.
1674
- return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
1675
-
1676
- }
1677
-
1678
-
1679
- /**
1680
- * When an object property is accessed, a new Proxy with a new instance of this handler class is created,
1681
- * but it tracks the path from the root to the property.
1682
- * That way when a property is set, it can report the changed path. */
1683
- class ProxyHandler$1 {
1684
-
1685
- /**
1686
- * @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
1687
- * @param path {string[]} Used internally. */
1688
- constructor(root, path=[]) {
1689
-
1690
- this.root = root;
1691
- this.path = path; // path from root to this Proxy.
1692
- }
1693
-
1694
- /**
1695
- * @param obj {Object}
1696
- * @param prop {string} */
1697
- get(obj, prop) {
1698
-
1699
- // Special props. Currently unused.
1700
- // if (prop === '$path')
1701
- // return this.path;
1702
- // if (prop === '$root')
1703
- // return this.root;
1704
- if (prop === '$isProxy')
1705
- return true;
1706
-
1707
-
1708
- // 1. Array.splice()
1709
- if (prop === 'splice' && Array.isArray(obj)) {
1710
- return (index, deleteCount, ...items) => {
1711
- let ngm = NodeGroupManager.get(this.root);
1712
-
1713
- if (deleteCount) {
1714
-
1715
- // Get the hash of each object along the delete range. The process to get the hash is:
1716
- // Serialized Path -> transformer -> Template -> hash.
1717
- let hashes = [];
1718
- for (let i=index; i<index+deleteCount; i++) {
1719
- let serializedPath = serializePath([this.root, ...this.path, i+'']);
1720
-
1721
- let obj = delve(this.root, [...this.path, i]);
1722
- for (let transformerInfo of pathToTransformer.getAll(serializedPath)) {
1723
- let template = transformerInfo.transformer(obj);
1724
- let hash = getObjectHash(template);
1725
- hashes.push(hash); // Hashes may go to nodes in more than one loop.
1726
- }
1727
- }
1728
-
1729
- ngm.changes.push(new Change('delete', this.root, [...this.path, index+''], hashes));
1730
- }
1731
-
1732
- //let oldArray = [...obj];
1733
- let result = obj.splice(index, deleteCount);
1734
-
1735
- // Inserting
1736
- if (items.length) {
1737
-
1738
- let beforeNgs;
1739
- for (let loopInfo of ngm.getLoopInfo([this.root, ...this.path])) {
1740
- let beforeObj = delve(this.root, [...this.path, index]);
1741
-
1742
- // Find where to insert before.
1743
- if (beforeObj) {
1744
- let beforeTemplate = loopInfo.itemTransformer(beforeObj);
1745
- let beforeHash = getObjectHash(beforeTemplate);
1746
- beforeNgs = ngm.nodeGroupsAvailable.data[beforeHash];
1747
-
1748
- if (beforeNgs) {
1749
- let hash = getObjectHash(loopInfo.template);
1750
- let loopNgs = ngm.nodeGroupsAvailable.data[hash] || [];
1751
- for (let loopNg of loopNgs)
1752
- for (let beforeNg of beforeNgs)
1753
- if (beforeNg.startNode.parentNode === loopNg.startNode.parentNode)
1754
- ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items, beforeTemplate));
1755
- }
1756
- }
1757
- if (!beforeNgs)
1758
- ngm.changes.push(new Change('insert', this.root, [...this.path, index + ''], items));
1759
- }
1760
- obj.splice(index, 0, ...items);
1761
- }
1762
- return result;
1763
- }
1764
- }
1765
-
1766
-
1767
-
1768
- // 2. Get property
1769
- // If we're getting an object or array property, apply watch() to it recursively.
1770
- let result = Reflect.get(obj, prop);
1771
- if (result && typeof result === 'object') {
1772
- let handler = new ProxyHandler$1(this.root, [...this.path, prop]); // same root, one level deeper on the path.
1773
- return new Proxy(result, handler);
1774
- }
1775
-
1776
- return result;
1777
- }
1778
-
1779
-
1780
- set(obj, prop, newValue) {
1781
- let ngm = NodeGroupManager.get(this.root);
1782
- ngm.changes.push(new Change('set', this.root, [...this.path, prop], newValue));
1783
- return Reflect.set(obj, prop, newValue)
1784
- }
1785
- }
1786
-
1787
-
1788
- /**
1789
- *
1790
- */
1791
- class Change {
1792
-
1793
- /**
1794
- * @param action {string}
1795
- * @param root {Object|Array}
1796
- * @param path {string[]}
1797
- * @param value
1798
- * If setting a value, this is the new value.
1799
- * If deleting from an array, this is an array of all the NodeGroups to delete.
1800
- *
1801
- * @param beforeTemplate
1802
- * */
1803
- constructor(action, root, path, value, beforeTemplate=null) {
1804
- this.action = action;
1805
-
1806
- // TODO: Store root as first item of path, to be consistent with code elsewhere.
1807
- this.root = root;
1808
- this.path = path;
1809
- this.value = value;
1810
- this.beforeTemplate = beforeTemplate;
1811
-
1812
- /** @type {TransformerInfo[]} */
1813
- this.transformerInfo = [];
1814
-
1815
- // Traverse up the path.
1816
- for (let i=this.path.length; i>0; i--) {
1817
- let path = this.path.slice(0, i);
1818
- let fullPath = [this.root, ...path];
1819
-
1820
- let serializedPath = getObjectHash(fullPath); // TODO: Why not serializedPath() ?
1821
- this.transformerInfo.push(...pathToTransformer.getAll(serializedPath));
1822
- }
1823
- }
1824
- }
1825
-
1826
- let logGets = false;
1827
- let gets = [];
1828
-
1829
- let withinSet = 0;
1830
-
1831
- /**
1832
- * Turn the props on obj into JavasCript properties that return Proxies when accessed.
1833
- * If called more than once, return the already-converted object.
1834
- * @param obj {Object}
1835
- * @param props {string}
1836
- * @returns {*|{$proxyHandler}}
1837
- */
1838
- function watch(obj, ...props) {
1839
-
1840
- if (props.length) {
1841
- let internalProps = {};
1842
- for (let prop of props) {
1843
- internalProps[prop] = obj[prop];
1844
- Object.defineProperty(obj, prop, {
1845
- get() {
1846
- return new Proxy(obj, new ProxyHandler(obj, [], internalProps))[prop];
1847
- },
1848
- set(value) {
1849
- return watch(this)[prop] = value;
1850
- }
1851
- });
1852
- }
1853
- return;
1854
- }
1855
-
1856
-
1857
- if (obj?.$proxyHandler)
1858
- return obj; // It's already a Proxy.
1859
-
1860
- // This cache doesn't make things faster.
1861
- // But could it save memory?
1862
- // let result = proxyCache.get(obj);
1863
- // if (!result) {
1864
- // result = new Proxy(obj, new ProxyHandler(obj));
1865
- // proxyCache.set(obj, result);
1866
- // }
1867
- // return result;
1868
-
1869
- return new Proxy(obj, new ProxyHandler(obj));
1870
- }
1871
-
1872
- /**
1873
- * Provides methods used when a Proxied version of a property is accessed on an object returned by watch() */
1874
- class ProxyHandler {
1875
-
1876
- serializedPath;
1877
-
1878
- /**
1879
- * @param root An element managed by a NodeGroupManager. The same as the NodeGroupManager's rootEl.
1880
- * @param path {string[]} Used internally.
1881
- * @param props */
1882
- constructor(root, path=[], props=null) {
1883
-
1884
- this.root = root;
1885
- this.path = path; // path from root to this Proxy.
1886
- this.props = props;
1887
- }
1888
-
1889
- /**
1890
- * Get the full path to this property from the root watched object.
1891
- * @param atIndex
1892
- * @returns {string} */
1893
- getSerializedPath(atIndex=null) {
1894
- if (!this.serializedPath)
1895
- this.serializedPath = JSON.stringify([getObjectId(this.root), ...this.path.map(item => item + '')]);
1896
-
1897
- if (atIndex!== null)
1898
- return this.serializedPath.slice(0, -1) + ',"' + atIndex + '"]';
1899
- return this.serializedPath;
1900
- }
1901
-
1902
- /**
1903
- * Return a ProxyHandler for a property one level deeper at pathItem.
1904
- * @param pathItem {string}
1905
- * @returns {ProxyHandler} */
1906
- extend(pathItem) {
1907
- pathItem += '';
1908
- assert(!this.root.$proxyHandler);
1909
- let result = new ProxyHandler(this.root, [...this.path, pathItem], this.props);
1910
- if (this.serializedPath)
1911
- result.serializedPath = this.getSerializedPath(pathItem);
1912
-
1913
- return result;
1914
- }
1915
-
1916
- /**
1917
- * Called directly by JavaScript when accessing the value of a property.
1918
- * @param obj {Object}
1919
- * @param prop {string} */
1920
- get(obj, prop) {
1921
-
1922
- // 1. Special props.
1923
- if (prop === '$proxyHandler')
1924
- return this;
1925
- else if (prop === '$removeProxy')
1926
- return delve(this.props || this.root, this.path);
1927
-
1928
- // 2. Array functions.
1929
- else if (prop === 'map' && Array.isArray(obj)) {
1930
-
1931
- let ngm = NodeGroupManager.get(this.root);
1932
- ngm.clearSubscribersIfNeeded();
1933
-
1934
- return callback => {
1935
- let loopInfo;
1936
-
1937
- let children = [];
1938
- let transformer = obj => {
1939
- let templates = [];
1940
-
1941
- for (let i = 0; i < obj.length; i++) {
1942
-
1943
- // Watch obj[i].
1944
- let handler = this.extend(i);
1945
- let item = obj[i];
1946
- if (!item.$proxyHandler)
1947
- item = new Proxy(item, handler);
1948
-
1949
- let template = callback(item, i, obj);
1950
- templates.push(template);
1951
-
1952
- // If the loop is re-evaluted via Set() then we add duplicate TemplateInfo's
1953
- //if (!withinSet) {
1954
- let spath = this.getSerializedPath(i);
1955
- let subscriber = new Subscriber(callback, template);
1956
- subscriber.parent = loopInfo;
1957
- ngm.subscribers.add(spath, subscriber);
1958
- children.push([spath, subscriber]);
1959
- //}
1960
- }
1961
-
1962
- // A parent Template that surrounds all the items in the loop.
1963
- // This lets us get template.nodeGroup.endNode so we can insertBefore().
1964
- return new Template(['', ''], [templates]);
1965
- };
1966
-
1967
- if (!withinSet)
1968
- loopInfo = new Subscriber(transformer, null, callback);
1969
- let wholeLoopTemplate = transformer(obj);
1970
- if (!withinSet) {
1971
- loopInfo.template = wholeLoopTemplate;
1972
- loopInfo.children = children;
1973
- ngm.subscribers.add(this.getSerializedPath(), loopInfo);
1974
- }
1975
- return wholeLoopTemplate;
1976
- }
1977
- }
1978
-
1979
- else if ((prop ==='splice' || prop === 'fastSplice') && Array.isArray(obj)) {
1980
- let ngm = NodeGroupManager.get(this.root);
1981
- return (index, deleteCount, ...items) => {
1982
- let diff = items.length - deleteCount;
1983
- let objLength = obj.length;
1984
-
1985
- // Delete
1986
- if (deleteCount) {
1987
- for (let i=index; i<index+deleteCount; i++) {
1988
-
1989
- // Update pathToTemplates
1990
- let spath = this.getSerializedPath(i);
1991
-
1992
- // Delete nodes of associated NodeGroups.
1993
- for (let subscriber of ngm.subscribers.data[spath] || []) {
1994
- let ng = subscriber.template.nodeGroup;
1995
- for (let node of ng.getNodes())
1996
- node.remove();
1997
-
1998
- // Delete NodeGroup from NodeGroupManager.
1999
- ngm.nodeGroupsAvailable.delete(ng.exactKey, ng);
2000
- }
2001
-
2002
- delete ngm.subscribers.data[spath]; // Deletes templates associated with every loop where this is used.
2003
- }
2004
- }
2005
-
2006
- // Update indices of subsequent items.
2007
- if (diff) {
2008
- let loopPath = this.getSerializedPath();
2009
- let loopInfo = [...ngm.subscribers.getAll(loopPath)][0]; // TODO: Handle multiple loops.
2010
-
2011
- let move = (oldIndex) => {
2012
- let newIndex = oldIndex+diff;
2013
- let oldPath = this.getSerializedPath(oldIndex);
2014
- let newPath = this.getSerializedPath(newIndex);
2015
-
2016
- let subscribers = ngm.subscribers.data[oldPath];
2017
- delete ngm.subscribers.data[oldPath]; // TODO: Some can be overwritten w/o being deleted?
2018
- ngm.subscribers.data[newPath] = subscribers;
2019
-
2020
-
2021
- // Update associated NodeGroups by passing them newIndex.
2022
- // This is unnecessary for most loops since they don't use the index.
2023
- // fastSplice skips this path, it skips updating item indices.
2024
- if (prop === 'splice') {
2025
- let array = delve(this.root, this.path);
2026
- for (let subscriber of subscribers) {
2027
- let ng = subscriber.template.nodeGroup;
2028
-
2029
- let item = array[oldIndex];
2030
-
2031
- //assert(!item.$proxyHandler)
2032
- let proxyItem = getProxy(item, this, this.path, newIndex); //new Proxy(item, this.extend(newIndex));
2033
- let exprs = loopInfo.itemTransformer(proxyItem, newIndex).exprs; // TODO: Pass updated array as third argument to transformer.
2034
- ng.applyExprs(exprs); // this is the slow part.
2035
- }
2036
- }
2037
- };
2038
-
2039
- // Iterate in different directions depending on whether diff is positive or negative.
2040
- if (diff > 0) // Moving items to the right, so we iterate backward from the end.
2041
- for (let i = objLength-1; i >= index + items.length + deleteCount; i--)
2042
- move(i);
2043
-
2044
- else // Moving items to the left, so we iterate forward.
2045
- for (let i = index + items.length + deleteCount; i < objLength; i++)
2046
- move(i);
2047
- }
2048
-
2049
-
2050
- // Add new items
2051
- if (items.length) {
2052
- let loopPath = this.getSerializedPath();
2053
-
2054
- let beforePath = this.getSerializedPath(index);
2055
- let ngm = NodeGroupManager.get(this.root);
2056
-
2057
- for (let loopInfo of ngm.subscribers.getAll(loopPath)) {
2058
- let beforeNodes = index < objLength - deleteCount
2059
- ? [...ngm.subscribers.getAll(beforePath)].map(t => t.template.nodeGroup.startNode)
2060
- : [loopInfo.template.nodeGroup.endNode];
2061
- for (let beforeNode of beforeNodes) { // TODO: Need to match the beforeNg with the loopInfo instead of iterating.
2062
- for (let i = 0; i < items.length; i++) {
2063
-
2064
- // Create NodeGroup of new item.
2065
- let itemHandler = this.extend(index + i);
2066
- assert(!items[i].$proxyHandler);
2067
- let proxyItem = new Proxy(items[i], itemHandler);
2068
- let template = loopInfo.itemTransformer(proxyItem);
2069
- let ng = ngm.getNodeGroup(template, null, true);
2070
-
2071
-
2072
- for (let node of ng.getNodes()) {
2073
- beforeNode.parentNode.insertBefore(node, beforeNode);
2074
- //loopInfo.template.nodeGroup.endNode = node; // The loop's end node is actually an empty text node, so don't do this.
2075
- }
2076
-
2077
-
2078
- // Add new items to ngm.templateInfo
2079
- let spath = itemHandler.getSerializedPath();
2080
- let subscriber = new Subscriber(loopInfo.itemTransformer, template);
2081
- subscriber.parent = loopInfo;
2082
- ngm.subscribers.add(spath, subscriber);
2083
- }
2084
- }
2085
- loopInfo.template.nodeGroup.parentPath.clearNodesCache();
2086
- loopInfo.template.nodeGroup.nodesCache = null;
2087
- }
2088
- }
2089
-
2090
- let result = obj.splice(index, deleteCount, ...items);
2091
-
2092
- //this.notify(this.path);
2093
-
2094
- return result;
2095
- }
2096
- }
2097
-
2098
- // Allow these functions to use proxied objects as arguments.
2099
- else if (prop ==='indexOf' && Array.isArray(obj)) {
2100
- return item => {
2101
- return obj.indexOf(item.$removeProxy || item)
2102
- }
2103
- }
2104
-
2105
- // 3. Get property
2106
- else {
2107
-
2108
- let obj2 = obj === this.root && this.props ? this.props : obj;
2109
- let result = Reflect.get(obj2, prop);
2110
-
2111
- // This is read by watchFunction() which is called in NodeGroup.applyOneExpr().
2112
- // It's used to see what variables contribute to an expression.
2113
- if (logGets)
2114
- gets.push([this.root, ...this.path, prop]);
2115
-
2116
- // If we're getting an object or array property, apply watch() to it recursively.
2117
- if (result && typeof result === 'object') {
2118
- let handler = this.extend(prop); // same root, one level deeper on the path.
2119
-
2120
- return new Proxy(result, handler);
2121
- }
2122
-
2123
- return result;
2124
- }
2125
- }
2126
-
2127
-
2128
- /**
2129
- * Called directly by JavaScript when setting the value of a property via equals.
2130
- * @param obj
2131
- * @param prop
2132
- * @param value
2133
- * @returns {boolean} */
2134
- set(obj, prop, value) {
2135
- withinSet++;
2136
-
2137
- let obj2 = obj === this.root && this.props ? this.props : obj;
2138
- let result = Reflect.set(obj2, prop, value);
2139
- let fullPath = [...this.path, prop+''];
2140
- this.notify(fullPath);
2141
-
2142
- withinSet --;
2143
-
2144
- return result;
2145
- }
2146
-
2147
- /**
2148
- * Find every subscriber for fullPath, and above, and call applyExprs() for it.
2149
- * @param fullPath {string[]}
2150
- * @param excluded {Set} */
2151
- notify(fullPath, excluded = new Set()) {
2152
-
2153
- // Traverse upward through the path, looking for pathToTemplates.
2154
- let ngm = NodeGroupManager.get(this.root);
2155
-
2156
- let rootHash = getObjectId(this.root);
2157
-
2158
- let len = fullPath.length;
2159
- while (len >= 1) {
2160
- let path = fullPath.slice(0, len);
2161
- let val = delve(this.root, path);
2162
- let serializedPath = JSON.stringify([rootHash, ...path]);
2163
- for (let subscriber of ngm.subscribers.getAll(serializedPath)) {
2164
- // We already applied expressions for a single item within this loop.
2165
- if (excluded.has(subscriber))
2166
- continue;
2167
-
2168
-
2169
- // Delete child subscriptions so we don't have duplicate subscriptions when we call applyExprs() directly below.
2170
- if (subscriber.children) {
2171
- for (let [spath, childInfo] of subscriber.children)
2172
- ngm.subscribers.delete(spath, childInfo);
2173
- subscriber.children = undefined;
2174
- }
2175
-
2176
- let proxyVal = getProxy(val, this, path);
2177
- let exprs = subscriber.transformer(proxyVal).exprs; // TODO: Pass updated array as third argument to transformer.
2178
- for (let path of subscriber.template.nodeGroup.paths)
2179
- path.clearNodesCache();
2180
-
2181
- // Apply expressions.
2182
- subscriber.template.nodeGroup.applyExprs(exprs);
2183
-
2184
- // Don't also process parent loop after updating a single item within it.
2185
- if (subscriber.parent)
2186
- excluded.add(subscriber.parent);
2187
- }
2188
- len--;
2189
- }
2190
- }
2191
- }
2192
-
2193
- function getProxy(obj, ph, path, path2) {
2194
- if (!obj || !typeof obj !== 'object')
2195
- return obj;
2196
-
2197
- if (obj.$proxyHandler) {
2198
-
2199
- assert(obj.$proxyHandler.root === ph.root && JSON.stringify(obj.$proxyHandler.path) === JSON.stringify(path));
2200
- return obj;
2201
- }
2202
- if (path2)
2203
- path = [...path, path2+''];
2204
- return new Proxy(obj, new ProxyHandler(ph.root, path, ph.props));
2205
- }
2206
-
2207
- /**
2208
- * Call a function and record which watched variables it accesess, storing their paths in pathToTemplates.
2209
- * Used by NodeGroup.applyOneExpr().
2210
- * TODO: only allow this to be called once per callback.
2211
- * @param callback {function}
2212
- * @param ngm {NodeGroupManager}
2213
- * @returns {Template} */
2214
- function watchFunction(callback, ngm) {
2215
- ngm.clearSubscribersIfNeeded();
2216
-
2217
- logGets = true;
2218
-
2219
- let transformer = () => new Template(['', ''], [callback()]);
2220
- let template = transformer();
2221
- for (let path of gets) {
2222
- let subscriber = new Subscriber(transformer, template);
2223
- ngm.subscribers.add(serializePath(path), subscriber);
2224
- }
2225
-
2226
- gets = [];
2227
- logGets = false;
2228
- return template;
2229
- }
2230
-
2231
- /**
2232
- * Represents a place where nodes will be updated.
2233
- * TODO: Merge this with Template, or ExprPath? */
2234
- class Subscriber {
2235
-
2236
- /** @type {Subscriber} Used only for children of a loop. */
2237
- parent;
2238
-
2239
- /** @type {Subscriber[]} TemplateInfo for each child of a loop. */
2240
- children;
2241
-
2242
- /**
2243
- * @param transformer {function} Function that turns the object at the path into a template.
2244
- * @param template {Template}
2245
- * @param itemTransformer {function} If a loop, this transforms each item in the loop. */
2246
- constructor(transformer, template, itemTransformer=null) {
2247
- this.transformer = transformer;
2248
- this.template = template;
2249
-
2250
- // Used only for loops
2251
- this.itemTransformer = itemTransformer;
2252
- }
2253
- }
1732
+ return b;
1733
+ };
2254
1734
 
2255
1735
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node} Expr */
2256
1736
 
@@ -2281,10 +1761,10 @@ class NodeGroup {
2281
1761
  /** @type {ExprPath[]} */
2282
1762
  paths = [];
2283
1763
 
2284
- /** @type {string} */
1764
+ /** @type {string} Key that matches the template and the expressions. */
2285
1765
  exactKey;
2286
1766
 
2287
- /** @type {string} */
1767
+ /** @type {string} Key that only matches the template. */
2288
1768
  closeKey;
2289
1769
 
2290
1770
  /** @type {boolean} Used by NodeGroupManager. */
@@ -2317,8 +1797,10 @@ class NodeGroup {
2317
1797
  * @returns {NodeGroup} */
2318
1798
  constructor(template, manager=null) {
2319
1799
 
2320
- // Used for forEach()
1800
+ /** @type {Template} */
2321
1801
  this.template = template;
1802
+
1803
+ /** @type {NodeGroupManager} */
2322
1804
  this.manager = manager;
2323
1805
 
2324
1806
  // new!
@@ -2334,7 +1816,7 @@ class NodeGroup {
2334
1816
  let replaceMode = typeof template.replaceMode === 'boolean'
2335
1817
  ? template.replaceMode
2336
1818
  : fragment.children.length===1 &&
2337
- fragment.firstElementChild?.tagName.replace(/-REDCOMPONENT-PLACEHOLDER$/, '')
1819
+ fragment.firstElementChild?.tagName.replace(/-SOLARITE-PLACEHOLDER$/, '')
2338
1820
  === manager?.rootEl?.tagName;
2339
1821
  if (replaceMode) {
2340
1822
  this.pseudoRoot = fragment.firstElementChild;
@@ -2361,7 +1843,7 @@ class NodeGroup {
2361
1843
 
2362
1844
 
2363
1845
  // Update web component placeholders.
2364
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1846
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2365
1847
  // Is this list needed at all?
2366
1848
  //for (let component of shell.components)
2367
1849
  // this.components.push(resolveNodePath(this.startNode.parentNode, getNodePath(component)))
@@ -2390,13 +1872,19 @@ class NodeGroup {
2390
1872
  this.createNewComponent(el);
2391
1873
  }
2392
1874
 
2393
- if (this.manager.rootEl) {
1875
+ if (this.manager?.rootEl) {
2394
1876
 
2395
1877
  // ids
2396
1878
  if (this.manager.options.ids !== false)
2397
1879
  for (let path of shell.ids) {
2398
1880
  let el = resolveNodePath(root, path);
2399
1881
  let id = el.getAttribute('data-id') || el.getAttribute('id');
1882
+
1883
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
1884
+ if (this.manager.rootEl[id] && !(this.manager.rootEl[id] instanceof Node))
1885
+ throw new Error(`${this.manager.rootEl.constructor.name}.${id} already has a value. `+
1886
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
1887
+
2400
1888
  this.manager.rootEl[id] = el;
2401
1889
  }
2402
1890
 
@@ -2421,20 +1909,13 @@ class NodeGroup {
2421
1909
  }
2422
1910
  }
2423
1911
 
2424
- updateStyles() {
2425
- if (this.styles)
2426
- for (let [style, oldText] of this.styles) {
2427
- let newText = style.textContent;
2428
- if (oldText !== newText)
2429
- Util.bindStyles(style, this.manager.rootEl);
2430
- }
2431
- }
2432
-
2433
1912
  /**
2434
1913
  * Use the paths to insert the given expressions.
2435
1914
  * Dispatches expression handling to other functions depending on the path type.
2436
- * @param exprs {(*|*[]|function|Template)[]} */
2437
- applyExprs(exprs) {
1915
+ * @param exprs {(*|*[]|function|Template)[]}
1916
+ * @param paths {?ExprPath[]} Optional. */
1917
+ applyExprs(exprs, paths=null) {
1918
+ paths = paths || this.paths;
2438
1919
 
2439
1920
 
2440
1921
  // Update exprs at paths.
@@ -2442,7 +1923,7 @@ class NodeGroup {
2442
1923
 
2443
1924
  // We apply them in reverse order so that a <select> box has its options created from an expression
2444
1925
  // before its value attribute is set via an expression.
2445
- for (let path of this.paths.toReversed()) {
1926
+ for (let path of paths.toReversed()) {
2446
1927
  expr = exprs[exprIndex];
2447
1928
 
2448
1929
  // Nodes
@@ -2453,8 +1934,8 @@ class NodeGroup {
2453
1934
 
2454
1935
  // Attributes
2455
1936
  else {
2456
- let node = path.nodeMarker; // path.resolve(result);
2457
- let node2 = (this.manager.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
1937
+ let node = path.nodeMarker;
1938
+ let el = (this.manager?.rootEl && node === this.pseudoRoot) ? this.manager.rootEl : node;
2458
1939
 
2459
1940
 
2460
1941
  // This is necessary both here and below.
@@ -2464,10 +1945,10 @@ class NodeGroup {
2464
1945
  }
2465
1946
 
2466
1947
  if (path.type === PathType.Multiple)
2467
- path.applyMultipleAttribs(node2, expr);
1948
+ path.applyMultipleAttribs(el, expr);
2468
1949
 
2469
1950
  // Capture attribute expressions to later send to the constructor of a web component.
2470
- // Ctrl+F "redcomponent-placeholder" in project to find all code that manages subcomponents.
1951
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2471
1952
  else if (path.nodeMarker !== this.pseudoRoot && path.type === PathType.Component)
2472
1953
  this.currentComponentProps[path.attrName] = expr;
2473
1954
 
@@ -2476,13 +1957,13 @@ class NodeGroup {
2476
1957
 
2477
1958
  // Event attribute value
2478
1959
  if (path.attrValue===null && (typeof expr === 'function' || Array.isArray(expr)) && isEvent(path.attrName)) {
2479
- let root = this.manager.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
2480
- path.applyEventAttrib(node2, expr, root);
1960
+ let root = this.manager?.rootEl || this.startNode.parentNode; // latter is used when constructing a whole element.
1961
+ path.applyEventAttrib(el, expr, root);
2481
1962
  }
2482
1963
 
2483
1964
  // Regular attribute value.
2484
- else
2485
- exprIndex = path.applyValueAttrib(node2, exprs, exprIndex);
1965
+ else // One node value may have multiple expressions. Here we apply them all at once.
1966
+ exprIndex = path.applyValueAttrib(el, exprs, exprIndex);
2486
1967
  }
2487
1968
 
2488
1969
  lastNode = path.nodeMarker;
@@ -2509,6 +1990,10 @@ class NodeGroup {
2509
1990
 
2510
1991
 
2511
1992
 
1993
+ }
1994
+
1995
+ applyExpr(path, expr) {
1996
+ // TODO: Use this if I can figure out how to adapt applyValueAttrib() to it.
2512
1997
  }
2513
1998
 
2514
1999
  /**
@@ -2532,7 +2017,7 @@ class NodeGroup {
2532
2017
  //for (let ng of path.nodeGroups) // TODO: Is this necessary?
2533
2018
  // ng.parentPath = null;
2534
2019
  path.nodeGroups = [];
2535
- this.applyOneExpr(expr, path, newNodes, secondPass);
2020
+ path.apply(expr, newNodes, secondPass);
2536
2021
  this.existingTextNodes = null;
2537
2022
 
2538
2023
  // TODO: Create an array of old vs Nodes and NodeGroups together.
@@ -2622,113 +2107,32 @@ class NodeGroup {
2622
2107
  }
2623
2108
  }
2624
2109
 
2625
- // TODO: Move to ExprPath?
2626
- applyOneExpr(expr, path, newNodes, secondPass) {
2627
-
2628
- if (expr instanceof Template) {
2629
- expr.parentPath = path;
2630
- expr.nodegroup = this;
2631
-
2632
- //if (window.debug && expr.exprs[0] === 'Banana' && path.nodeGroups.length === 0)
2633
- //if (window.debug && expr.exprs[0] === 'Banana')
2634
- // debugger;
2635
-
2636
- let ng = this.manager.getNodeGroup(expr, true);
2637
-
2638
-
2639
- if (ng) {
2640
-
2641
-
2642
-
2643
- // TODO: Track ranges of changed nodes and only pass those to udomdiff?
2644
- // But will that break the swap benchmark?
2645
- newNodes.push(...ng.getNodes());
2646
- path.nodeGroups.push(ng);
2647
- }
2648
-
2649
- // If expression, evaluate later to find partial match.
2650
- else {
2651
- secondPass.push([newNodes.length, path.nodeGroups.length]);
2652
- newNodes.push(expr);
2653
- path.nodeGroups.push(null); // placeholder
2654
- }
2655
- }
2656
-
2657
- // Node created by an expression.
2658
- else if (expr instanceof Node) {
2659
-
2660
- // DocumentFragment created by an expression.
2661
- if (expr instanceof DocumentFragment)
2662
- newNodes.push(...expr.childNodes);
2663
- else
2664
- newNodes.push(expr);
2665
- }
2666
-
2667
- else if (Array.isArray(expr))
2668
- for (let subExpr of expr)
2669
- this.applyOneExpr(subExpr, path, newNodes, secondPass);
2670
-
2671
- else if (typeof expr === 'function') {
2672
- expr = watchFunction(expr, this.manager);
2673
-
2674
- this.applyOneExpr(expr, path, newNodes, secondPass);
2675
- }
2676
-
2677
- // Text
2678
- else {
2679
- // Convert falsy values (but not 0) to empty string.
2680
- // Convert numbers to string so they compare the same.
2681
- let text = (expr === undefined || expr === false || expr === null) ? '' : expr + '';
2682
-
2683
- // Fast path for updating the text of a single text node.
2684
- let first = path.nodeBefore.nextSibling;
2685
- if (first.nodeType === 3 && first.nextSibling === path.nodeMarker && !newNodes.includes(first)) {
2686
- if (first.textContent !== text)
2687
- first.textContent = text;
2688
-
2689
- newNodes.push(first);
2690
- }
2691
-
2692
- else {
2693
- // TODO: Optimize this into a Set or Map or something?
2694
- if (!this.existingTextNodes)
2695
- this.existingTextNodes = path.getNodes().filter(n => n.nodeType === 3);
2696
-
2697
- let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
2698
- if (idx !== -1)
2699
- newNodes.push(...this.existingTextNodes.splice(idx, 1));
2700
- else
2701
- newNodes.push(path.parentNode.ownerDocument.createTextNode(text));
2702
- }
2703
- }
2704
- }
2705
-
2706
2110
  /**
2707
2111
  * Create a nested RedComponent or call render with the new props.
2708
2112
  * @param el {Solarite:HTMLElement}
2709
2113
  * @param props {Object} */
2710
2114
  applyComponentExprs(el, props) {
2711
-
2115
+
2712
2116
  // TODO: Does a hash of this already exist somewhere?
2713
2117
  // Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
2714
2118
  // then we could re-use the hash and logic from NodeManager?
2715
2119
  let newHash = getObjectHash(props);
2716
-
2717
- let isPreHtmlElement = el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER');
2120
+
2121
+ let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
2718
2122
  let isPreIsElement = el.hasAttribute('_is');
2719
-
2720
-
2123
+
2124
+
2721
2125
  // Instantiate a placeholder.
2722
2126
  if (isPreHtmlElement || isPreIsElement)
2723
2127
  el = this.createNewComponent(el, isPreHtmlElement, props);
2724
-
2128
+
2725
2129
  // Update params of placeholder.
2726
2130
  else if (el.render) {
2727
2131
  let oldHash = componentHash.get(el);
2728
2132
  if (oldHash !== newHash)
2729
2133
  el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
2730
2134
  }
2731
-
2135
+
2732
2136
  componentHash.set(el, newHash);
2733
2137
  }
2734
2138
 
@@ -2747,8 +2151,8 @@ class NodeGroup {
2747
2151
  isPreHtmlElement = !el.hasAttribute('_is');
2748
2152
 
2749
2153
  let tagName = (isPreHtmlElement
2750
- ? el.tagName.endsWith('-REDCOMPONENT-PLACEHOLDER')
2751
- ? el.tagName.slice(0, -25)
2154
+ ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
2155
+ ? el.tagName.slice(0, -21)
2752
2156
  : el.tagName
2753
2157
  : el.getAttribute('is')).toLowerCase();
2754
2158
 
@@ -2868,18 +2272,38 @@ class NodeGroup {
2868
2272
  }
2869
2273
 
2870
2274
 
2275
+
2276
+ updateStyles() {
2277
+ if (this.styles)
2278
+ for (let [style, oldText] of this.styles) {
2279
+ let newText = style.textContent;
2280
+ if (oldText !== newText)
2281
+ Util.bindStyles(style, this.manager.rootEl);
2282
+ }
2283
+ }
2284
+
2285
+
2871
2286
 
2872
2287
  }
2873
2288
 
2874
2289
 
2875
2290
  let componentHash = new WeakMap();
2876
2291
 
2292
+ /**
2293
+ * Tools for watch variables and performing precise renders.
2294
+ */
2295
+
2296
+ function serializePath(path) {
2297
+ // Convert any array indices to strings, so serialized comparisons work.
2298
+ return JSON.stringify([getObjectId(path[0]), ...path.slice(1).map(item => item+'')])
2299
+
2300
+ }
2301
+
2877
2302
  /**
2878
2303
  * @typedef {Object} RenderOptions
2879
2304
  * @property {boolean=} styles - Indicates whether the Courage component is present.
2880
2305
  * @property {boolean=} scripts - Indicates whether the Power component is present.
2881
- * @property {boolean=} ids
2882
- *
2306
+ * @property {boolean=} ids *
2883
2307
  * @property {?boolean} render
2884
2308
  * Used only when options are given to a class super constructor inheriting from Solarite.
2885
2309
  * True to call render() immediately in super constructor.
@@ -2928,122 +2352,12 @@ class NodeGroupManager {
2928
2352
  * @param rootEl {HTMLElement|DocumentFragment} If not specified, the first element of the html will be the rootEl. */
2929
2353
  constructor(rootEl=null) {
2930
2354
  this.rootEl = rootEl;
2355
+
2931
2356
  /*
2932
2357
 
2933
2358
  */
2934
2359
  }
2935
2360
 
2936
- /**
2937
- * Render the main template, which may indirectly call renderTemplate() to create children.
2938
- * @param template {Template}
2939
- * @param options {RenderOptions}
2940
- * @return {?DocumentFragment} */
2941
- render(template, options={}) {
2942
- this.mutationWatcherEnabled = false;
2943
- this.options = options;
2944
- this.clearSubscribers = false;
2945
-
2946
-
2947
-
2948
- if (!template && template !== '') {
2949
- this.rootEl.outerHTML = '';
2950
- this.mutationWatcherEnabled = true;
2951
- return null;
2952
- }
2953
-
2954
- // Fast path for empty component.
2955
- if (template.html?.length === 1 && !template.html[0]) {
2956
- this.rootEl.innerHTML = '';
2957
- }
2958
- else {
2959
-
2960
- // Find or create a NodeGroup for the template.
2961
- // This updates all nodes from the template.
2962
- let close;
2963
- let exact = this.getNodeGroup(template, true);
2964
- if (!exact) {
2965
- close = this.getNodeGroup(template, false);
2966
- }
2967
-
2968
-
2969
- let firstTime = !this.rootNg;
2970
- this.rootNg = exact || close;
2971
-
2972
- // Reparent NodeGroup
2973
- // TODO: Move this to NodeGroup?
2974
- let parent = this.rootNg.getParentNode();
2975
- if (!this.rootEl)
2976
- this.rootEl = parent;
2977
-
2978
- // If this is the first time rendering this element.
2979
- else if (firstTime) {
2980
-
2981
- // Save slot children
2982
- let fragment;
2983
- if (this.rootEl.childNodes.length) {
2984
- fragment = document.createDocumentFragment();
2985
- fragment.append(...this.rootEl.childNodes);
2986
- }
2987
-
2988
- // Add rendered elements.
2989
- if (parent instanceof DocumentFragment)
2990
- this.rootEl.append(parent);
2991
- else if (parent)
2992
- this.rootEl.append(...parent.childNodes);
2993
-
2994
- // Apply slot children
2995
- if (fragment) {
2996
- for (let slot of this.rootEl.querySelectorAll('slot[name]')) {
2997
- let name = slot.getAttribute('name');
2998
- if (name)
2999
- slot.append(...fragment.querySelectorAll(`[slot='${name}']`));
3000
- }
3001
- let unamedSlot = this.rootEl.querySelector('slot:not([name])');
3002
- if (unamedSlot)
3003
- unamedSlot.append(fragment);
3004
- }
3005
-
3006
- }
3007
-
3008
- // this.rootNg was rendered as childrenOnly=true
3009
- // Apply attributes from a root element to the real root element.
3010
- let ng = this.rootNg;
3011
- if (ng.pseudoRoot && ng.pseudoRoot !== this.rootEl) {
3012
-
3013
-
3014
- // Remove old attributes
3015
- // for (let attrib of this.rootEl.attributes)
3016
- // if (attrib.name !== 'is' && attrib.name !== 'data-style' && !ng.pseudoRoot.hasAttribute(attrib.name))
3017
- // this.rootEl.removeAttribute(attrib.name)
3018
-
3019
- // Add/set new attributes
3020
- if (firstTime)
3021
- for (let attrib of ng.pseudoRoot.attributes)
3022
- if (!this.rootEl.hasAttribute(attrib.name))
3023
- this.rootEl.setAttribute(attrib.name, attrib.value);
3024
-
3025
- // ng.startNode = ng.endNode = this.rootEl;
3026
- // ng.nodesCache = [ng.startNode]
3027
- // for (let path of ng.paths) {
3028
- // if (path.nodeMarker === ng.rootEl)
3029
- // path.nodeMarker = this.rootEl;
3030
- // path.nodesCache = null;
3031
- //
3032
- // }
3033
- //
3034
- // ng.rootEl = this.rootEl;
3035
- }
3036
-
3037
-
3038
- this.reset();
3039
-
3040
- }
3041
-
3042
- this.mutationWatcherEnabled = true;
3043
- return this.rootEl;
3044
-
3045
- }
3046
-
3047
2361
 
3048
2362
  /**
3049
2363
  *
@@ -3142,6 +2456,8 @@ class NodeGroupManager {
3142
2456
  // But it can still be a close match, so we don't use this code.
3143
2457
  success = this.nodeGroupsAvailable.delete(ng2.closeKey, ng2);
3144
2458
 
2459
+
2460
+ this.nodeGroupsInUse.push(ng2);
3145
2461
  }
3146
2462
  }
3147
2463
  }
@@ -3172,7 +2488,7 @@ class NodeGroupManager {
3172
2488
  * but don't reparent it if it's somewhere else.
3173
2489
  * @param template {Template}
3174
2490
  * @param exact {?boolean}
3175
- * @param createForWatch
2491
+ * @param createForWatch Deprecated.
3176
2492
  * @return {?NodeGroup} */
3177
2493
  getNodeGroup(template, exact=null, createForWatch=false) {
3178
2494
 
@@ -3224,7 +2540,7 @@ class NodeGroupManager {
3224
2540
  // Perhaps also result could cache its last exprKey and then we'd use only one map?
3225
2541
  ng.exactKey = exactKey;
3226
2542
  ng.closeKey = closeKey;
3227
- if (createForWatch) // TODO: Have this path be a separate function?
2543
+ if (createForWatch)
3228
2544
  this.nodeGroupsAvailable.add(ng.exactKey, ng);
3229
2545
  else
3230
2546
  this.nodeGroupsInUse.push(ng);
@@ -3267,14 +2583,7 @@ class NodeGroupManager {
3267
2583
  //pathToLoopInfo = new MultiValueMap(); // uses a Set() for each value.
3268
2584
  clearSubscribers = false;
3269
2585
 
3270
- /**
3271
- * One path may be used to loop in more than one place, so we use this to get every anchor from each loop.
3272
- * @param path {Array}
3273
- * @return {LoopInfo[]} A function that gets the loop anchor NodeGroup */
3274
- getLoopInfo(path) {
3275
- let serializedArrayPath = serializePath(path);
3276
- return [...this.pathToLoopInfo.getAll(serializedArrayPath)]; // This is set inside forEach()
3277
- }
2586
+
3278
2587
 
3279
2588
 
3280
2589
  /**
@@ -3302,7 +2611,10 @@ class NodeGroupManager {
3302
2611
  * Get the NodeGroupManager for a Web Component.
3303
2612
  * @param rootEl {Solarite|HTMLElement}
3304
2613
  * @return {NodeGroupManager} */
3305
- static get(rootEl) {
2614
+ static get(rootEl=null) {
2615
+ if (!rootEl)
2616
+ return new NodeGroupManager();
2617
+
3306
2618
  let ngm = nodeGroupManagers.get(rootEl);
3307
2619
  if (!ngm) {
3308
2620
  ngm = new NodeGroupManager(rootEl);
@@ -3321,16 +2633,7 @@ NodeGroupManager.pendingChildren = [];
3321
2633
  /**
3322
2634
  * Each Element that has Expr children has an associated NodeGroupManager here.
3323
2635
  * @type {WeakMap<HTMLElement, NodeGroupManager>} */
3324
- let nodeGroupManagers = new WeakMap();
3325
-
3326
-
3327
-
3328
- class LoopInfo {
3329
- constructor(loopTemplate, itemTransformer) {
3330
- this.template = loopTemplate;
3331
- this.itemTransformer = itemTransformer;
3332
- }
3333
- }
2636
+ let nodeGroupManagers = new WeakMap();
3334
2637
 
3335
2638
  /**
3336
2639
  * Convert strings to HTMLNodes.
@@ -3355,10 +2658,10 @@ class LoopInfo {
3355
2658
  * 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3356
2659
  * 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.
3357
2660
  * 8. r(template) // Render Template created by #1.
3358
- * 9. r(() => r`<b>Hello</b>`); // Create dynamic element that has a render() function.
2661
+ * 9. r(() => r`<b>Hello</b>`, {...}); // Create dynamic element that has a render() function.
3359
2662
  *
3360
2663
  * @param htmlStrings {?HTMLElement|string|string[]|function():Template}
3361
- * @param exprs {*[]|string|Template}
2664
+ * @param exprs {*[]|string|Template|Object}
3362
2665
  * @return {Node|HTMLElement|Template} */
3363
2666
  function r(htmlStrings=undefined, ...exprs) {
3364
2667
 
@@ -3372,9 +2675,8 @@ function r(htmlStrings=undefined, ...exprs) {
3372
2675
 
3373
2676
  // 2. Render template created by #4 to element.
3374
2677
  if (exprs[0] instanceof Template) {
3375
- let ngm = NodeGroupManager.get(parent);
3376
2678
  let options = exprs[1];
3377
- ngm.render(template, options);
2679
+ template.render(parent, options);
3378
2680
 
3379
2681
  // Append on the first go.
3380
2682
  if (!parent.childNodes.length && this) {
@@ -3393,15 +2695,16 @@ function r(htmlStrings=undefined, ...exprs) {
3393
2695
  return (htmlStrings, ...exprs) => {
3394
2696
  rendered.add(parent);
3395
2697
  let template = r(htmlStrings, ...exprs);
3396
- let ngm = NodeGroupManager.get(parent);
3397
- return ngm.render(template, options);
2698
+ return template.render(parent, options);
3398
2699
  }
3399
2700
  }
3400
2701
 
3401
2702
  // null for expr[0], remove whole element.
2703
+ // This path never happens?
3402
2704
  else {
3403
- let ngm = NodeGroupManager.get(parent);
3404
- ngm.render(null, exprs[1]);
2705
+ throw new Error('unsupported');
2706
+ //let ngm = NodeGroupManager.get(parent);
2707
+ //ngm.render(null, exprs[1])
3405
2708
  }
3406
2709
  }
3407
2710
 
@@ -3429,16 +2732,16 @@ function r(htmlStrings=undefined, ...exprs) {
3429
2732
  return (htmlStrings, ...exprs) => {
3430
2733
  //rendered.add(parent)
3431
2734
  let template = r(htmlStrings, ...exprs);
3432
- return template.toNode();
2735
+ return template.render();
3433
2736
  }
3434
2737
  }
3435
2738
 
3436
2739
  // 8.
3437
2740
  else if (htmlStrings instanceof Template) {
3438
- let ngm = new NodeGroupManager();
3439
- return ngm.render(htmlStrings);
2741
+ return htmlStrings.render();
3440
2742
  }
3441
2743
 
2744
+
3442
2745
  // 9. Create dynamic element with render() function.
3443
2746
  else if (typeof htmlStrings === 'function') {
3444
2747
  let getTemplate = htmlStrings;
@@ -3447,17 +2750,27 @@ function r(htmlStrings=undefined, ...exprs) {
3447
2750
  if (typeof template === 'string')
3448
2751
  throw new Error(`Please add the "r" prefix before the string "${template}"`)
3449
2752
 
3450
- let ngm = new NodeGroupManager();
3451
2753
  template.replaceMode = true;
3452
- let el = ngm.render(template);
2754
+ let el = template.render();
3453
2755
 
2756
+ // Create the render() function from the function we were given.
3454
2757
  el.render = (function() {
3455
2758
  template = getTemplate();
3456
- ngm.render(template);
2759
+ template.render(el);
3457
2760
  }).bind(el);
3458
2761
 
2762
+
2763
+ // The second argument was an object of additional properties to add.
2764
+ let props = exprs[0];
2765
+ for (let name in props)
2766
+ if (typeof props[name] === 'function')
2767
+ el[name] = props[name].bind(el);
2768
+ else
2769
+ el[name] = props[name];
2770
+
3459
2771
  return el;
3460
2772
  }
2773
+
3461
2774
  else
3462
2775
  throw new Error('Unsupported arguments.')
3463
2776
  }
@@ -3472,6 +2785,10 @@ function r(htmlStrings=undefined, ...exprs) {
3472
2785
  * @type {WeakSet<HTMLElement>} */
3473
2786
  let rendered = new WeakSet();
3474
2787
 
2788
+ //import {watchGet, watchSet} from "./watch.js";
2789
+
2790
+
2791
+
3475
2792
  function defineClass(Class, tagName, extendsTag) {
3476
2793
  if (!customElements.getName(Class)) { // If not previously defined.
3477
2794
  tagName = tagName || camelToDashes(Class.name);
@@ -3502,9 +2819,15 @@ let connected = new WeakSet();
3502
2819
  * 2. Calls render() when added to the DOM, if it hasn't been called already.
3503
2820
  * 3. Child elements are added before constructor is called. But they're also passed to the constructor.
3504
2821
  * 4. We can use this.html = r`...` to set html.
3505
- * 5. We have the onConnect, onFirstConnect, and onDisconnect methods. These could be standalone though.
2822
+ * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
2823
+ * Can't figure out how to have these work standalone though, and still be synchronous.
3506
2824
  * 6. Can we extend from other element types like TR?
3507
2825
  *
2826
+ * Advantages to inheriting from HTMLElement
2827
+ * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
2828
+ * 2. We can inherit from things like HTMLTableRowElement directly.
2829
+ * 3. There's less magic, since everyone is familiar with defining custom elements.
2830
+ *
3508
2831
  * @param extendsTag {?string}
3509
2832
  * @return {Class} */
3510
2833
  function createSolarite(extendsTag=null) {
@@ -3568,7 +2891,7 @@ function createSolarite(extendsTag=null) {
3568
2891
  if (ch)
3569
2892
  (this.querySelector('slot') || this).append(...ch);
3570
2893
 
3571
-
2894
+ /** @deprecated */
3572
2895
  Object.defineProperty(this, 'html', {
3573
2896
  set(html) {
3574
2897
  rendered.add(this);
@@ -3618,112 +2941,7 @@ function createSolarite(extendsTag=null) {
3618
2941
  defineClass(this, tagName, extendsTag);
3619
2942
  }
3620
2943
 
3621
-
3622
- renderWatched() {
3623
- let ngm = NodeGroupManager.get(this);
3624
-
3625
- let nodeGroupUpdates = [];
3626
-
3627
- for (let change of ngm.changes) {
3628
- if (change.action === 'set') {
3629
- for (let transformerInfo of change.transformerInfo) {
3630
-
3631
- let oldHash = transformerInfo.hash;
3632
-
3633
- let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
3634
- let newTemplate = transformerInfo.transformer(newObj);
3635
- let newHash = getObjectHash(newTemplate);
3636
- let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
3637
- for (let ng of ngs) {
3638
- nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
3639
- }
3640
- }
3641
- }
3642
-
3643
- else if (change.action === 'delete') {
3644
- for (let hash of change.value) {
3645
- let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
3646
-
3647
- for (let ng of ngs) {
3648
- if (ng.parentPath)
3649
- ng.parentPath.clearNodesCache();
3650
-
3651
- for (let node of ng.getNodes())
3652
- node.remove();
3653
-
3654
- // TODO: Update ancestor NodeGroup exactKeys
3655
- }
3656
- }
3657
- }
3658
- else if (change.action === 'insert') {
3659
-
3660
- let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
3661
- let arrayPath = [change.root, ...change.path];
3662
-
3663
- // Get anchor so we can use it to get the parent
3664
- // TODO: Should this be watchGet(change.root) ?
3665
- for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
3666
-
3667
- // Change.extra is aTemplate telling us where to insert before.
3668
- let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
3669
-
3670
- // Loop over every item added to the array.
3671
- let i = 0; // TODO: How to get real insert index.
3672
- for (let obj of change.value) {
3673
-
3674
- // Same logic as forEach() function.
3675
-
3676
- let callback = loopInfo.itemTransformer;
3677
- let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
3678
-
3679
- // Shortened logic found in watchGet(), but not any faster?
3680
- // the watchSet() is what makes this slower!
3681
- // let obj = delve(watchSet(path[0]), path.slice(1));
3682
- // let template = callback(obj);
3683
- // let serializedPath = serializePath(path);
3684
- // pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
3685
-
3686
- let template = watchGet(path, callback);
3687
- i++;
3688
-
3689
-
3690
- //let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
3691
-
3692
- // Create new NodeGroup
3693
- let ng = ngm.getNodeGroup(template, false, true);
3694
- ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
3695
-
3696
- for (let node of ng.getNodes())
3697
- beforeNode.parentNode.insertBefore(node, beforeNode);
3698
-
3699
- if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
3700
- ng.parentPath.clearNodesCache();
3701
- }
3702
-
3703
- // TODO: Update ancestor NodeGroup exactKeys
3704
- }
3705
- }
3706
- }
3707
-
3708
- // Update them all at once, that way we can reassign the same value twice.
3709
- for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
3710
- ng.applyExprs(exprs);
3711
- ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
3712
- ng.exactKey = ti.hash = newHash;
3713
- ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
3714
- }
3715
-
3716
-
3717
- ngm.changes = [];
3718
-
3719
- return []; // TODO
3720
- }
3721
2944
 
3722
- /**
3723
- * @deprecated Use the getArg() function instead. */
3724
- getArg(name, val=null, type=ArgType.String) {
3725
- return getArg(this, name, val, type);
3726
- }
3727
2945
  }
3728
2946
  }
3729
2947
 
@@ -3735,6 +2953,10 @@ let Solarite = new Proxy(createSolarite(), {
3735
2953
  return createSolarite(...args)
3736
2954
  }
3737
2955
  });
3738
- // unfinished
2956
+
2957
+
2958
+ //Experimental:
2959
+ //export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
2960
+ //export {watch} from './watch2.js'; // unfinished
3739
2961
 
3740
- export { ArgType, Solarite, forEach, getArg, r, watch, watchGet, watchSet };
2962
+ export { ArgType, Solarite, Template, getArg, r };