solarite 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -105,8 +105,9 @@ function reset() {
105
105
  Globals = {
106
106
 
107
107
  /**
108
- * Used by NodeGroup.applyComponentExprs() */
109
- componentArgsHash: new WeakMap(),
108
+ * Dynamic values that should be passed to a Component's constructor and render() function.
109
+ * @type {Map<HTMLElement, any[]>} */
110
+ componentArgs: new Map(),
110
111
 
111
112
  /**
112
113
  * Store which instances of Solarite have already been added to the DOM.
@@ -121,6 +122,9 @@ function reset() {
121
122
 
122
123
  div: document.createElement("div"),
123
124
 
125
+ /** @type {HTMLDocument} */
126
+ doc: document,
127
+
124
128
  /**
125
129
  * @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
126
130
  elementClasses: {},
@@ -139,20 +143,20 @@ function reset() {
139
143
  nodeGroups: new WeakMap(),
140
144
 
141
145
  /**
142
- * Used by r() path 9. */
146
+ * Used by h() path 9. */
143
147
  objToEl: new WeakMap(),
144
148
 
145
149
  //pendingChildren: [],
146
150
 
147
151
 
148
152
  /**
149
- * Elements that have been rendered to by r() at least once.
153
+ * Elements that have been rendered to by h() at least once.
150
154
  * This is used by the Solarite class to know when to call onFirstConnect()
151
155
  * @type {WeakSet<HTMLElement>} */
152
156
  rendered: new WeakSet(),
153
157
 
154
158
  /**
155
- * Elements that are currently rendering via the r() function.
159
+ * Elements that are currently rendering via the h() function.
156
160
  * @type {WeakSet<HTMLElement>} */
157
161
  rendering: new WeakSet(),
158
162
 
@@ -239,12 +243,25 @@ let Util = {
239
243
  return true; // the same.
240
244
  },
241
245
 
246
+ /**
247
+ * Convert HTMLElement attributes to an object.
248
+ * @param el {HTMLElement}
249
+ * @param ignore {?string} Optionally ignore this attribute.
250
+ * @return {Object} */
251
+ attribsToObject(el, ignore=null) {
252
+ let result = {};
253
+ for (let attrib of el.attributes)
254
+ if (attrib.name !== ignore)
255
+ result[Util.dashesToCamel(attrib.name)] = attrib.value;
256
+ return result;
257
+ },
258
+
242
259
  bindId(root, el) {
243
260
  let id = el.getAttribute('data-id') || el.getAttribute('id');
244
261
  if (id) { // If something hasn't removed the id.
245
262
 
246
263
  // Don't allow overwriting existing class properties if they already have a non-Node value.
247
- if (root[id] && !(root[id] instanceof Node))
264
+ if (root[id] && !(root[id]?.nodeType))
248
265
  throw new Error(`${root.constructor.name}.${id} already has a value. ` +
249
266
  `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
250
267
 
@@ -253,26 +270,50 @@ let Util = {
253
270
  },
254
271
 
255
272
  /**
273
+ * If the style tab has a global attribute:
274
+ * 1. Put it in the document head as <style data-style="tag-name">...</style>
275
+ * 2. Replace the :host {...} CSS selector as tag-name {...}.
276
+ * Otherwise keep it where it is and:
277
+ * 1. Add data-style="1" attribute to the root element.
278
+ * 2. Replace the :host {...} selector in the style as tag-name[data-style='1'] {...}
256
279
  * @param style {HTMLStyleElement}
257
280
  * @param root {HTMLElement} */
258
281
  bindStyles(style, root) {
259
- let styleId = root.getAttribute('data-style');
260
- if (!styleId) {
261
- // Keep track of one style id for each class.
262
- // TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
263
- if (!root.constructor.styleId)
264
- root.constructor.styleId = 1;
265
- styleId = root.constructor.styleId++;
266
282
 
267
- root.setAttribute('data-style', styleId);
283
+ let tagName = root.tagName.toLowerCase();
284
+ let styleId, attribSelector;
285
+
286
+ if (style.hasAttribute('global') || style.hasAttribute('data-global')) {
287
+ styleId = tagName;
288
+ attribSelector = '';
289
+ let doc = Globals$1.doc || root.ownerDocument || document;
290
+ if (!doc.head.querySelector(`style[data-style="${styleId}"]`)) {
291
+ doc.head.append(style);
292
+ style.setAttribute('data-style', styleId);
293
+ }
294
+ else // TODO: Make sure the style has no expressions.
295
+ style.remove(); // already in the head.
296
+ }
297
+ else {
298
+ let styleId = root.getAttribute('data-style');
299
+ if (!styleId) {
300
+ // Keep track of one style id for each class.
301
+ // TODO: Put this outside the class in a map, so it doesn't conflict with static properties.
302
+ if (!root.constructor.styleId)
303
+ root.constructor.styleId = 1;
304
+ styleId = root.constructor.styleId++;
305
+
306
+ root.setAttribute('data-style', styleId);
307
+ }
308
+
309
+ attribSelector = `[data-style="${styleId}"]`;
268
310
  }
269
311
 
270
312
  // Replace ":host" with "tagName[data-style=...]" in the css.
271
- let tagName = root.tagName.toLowerCase();
272
313
  for (let child of style.childNodes) {
273
314
  if (child.nodeType === 3) {
274
315
  let oldText = child.textContent;
275
- let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`);
316
+ let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}${attribSelector}`);
276
317
  if (oldText !== newText)
277
318
  child.textContent = newText;
278
319
  }
@@ -319,7 +360,6 @@ let Util = {
319
360
  return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
320
361
  },
321
362
 
322
-
323
363
  /**
324
364
  * A generator function that recursively traverses and flattens a value.
325
365
  *
@@ -360,7 +400,7 @@ let Util = {
360
400
 
361
401
  /**
362
402
  * Get the value of an input as the most appropriate JavaScript type.
363
- * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
403
+ * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLElement}
364
404
  * @return {string|string[]|number|[]|File[]|Date|boolean} */
365
405
  getInputValue(node) {
366
406
  // .type is a built-in DOM property
@@ -374,6 +414,8 @@ let Util = {
374
414
  return node.valueAsDate; // Date Object
375
415
  if (node.type === 'select-multiple') // <select multiple>
376
416
  return [...node.selectedOptions].map(option => option.value); // Array of Strings
417
+ if (node.hasAttribute('contenteditable'))
418
+ return node.innerHTML;
377
419
 
378
420
  return node.value; // String
379
421
  },
@@ -682,11 +724,6 @@ class MultiValueMap {
682
724
  * @returns {Node[]} The same list of future children.
683
725
  */
684
726
  const udomdiff = (parentNode, a, b, before) => {
685
- //#IFDEV
686
- // if (parentNode instanceof ExprPath)
687
- // parentNode.verify();
688
- //#ENDIF
689
-
690
727
  const bLength = b.length;
691
728
  let aEnd = a.length;
692
729
  let bEnd = bLength;
@@ -708,13 +745,6 @@ const udomdiff = (parentNode, a, b, before) => {
708
745
  while (bStart < bEnd) {
709
746
  let bNode = b[bStart++];
710
747
  parentNode.insertBefore(bNode, node);
711
-
712
- //#IFDEV
713
- if (bNode instanceof NodeGroup)
714
- bNode.verify();
715
- // if (parentNode instanceof ExprPath)
716
- // parentNode.verify();
717
- //#ENDIF
718
748
  }
719
749
  }
720
750
  // remove head or tail: fast path
@@ -724,13 +754,6 @@ const udomdiff = (parentNode, a, b, before) => {
724
754
  let aNode = a[aStart];
725
755
  if (!map || !map.has(aNode)) {
726
756
  parentNode.removeChild(aNode);
727
-
728
- //#IFDEV
729
- if (aNode instanceof NodeGroup)
730
- aNode.verify();
731
- // if (parentNode instanceof ExprPath)
732
- // parentNode.verify();
733
- //#ENDIF
734
757
  }
735
758
  aStart++;
736
759
  }
@@ -767,24 +790,10 @@ const udomdiff = (parentNode, a, b, before) => {
767
790
  a2,
768
791
  b2.nextSibling
769
792
  );
770
- //#IFDEV
771
- if (a2 instanceof NodeGroup)
772
- a2.verify();
773
- // if (parentNode instanceof ExprPath)
774
- // parentNode.verify();
775
- //#ENDIF
776
793
 
777
794
  let bNode = b[--bEnd];
778
795
  parentNode.insertBefore(bNode, node);
779
796
 
780
- //#IFDEV
781
- if (bNode instanceof NodeGroup)
782
- bNode.verify();
783
- // if (parentNode instanceof ExprPath)
784
- // parentNode.verify();
785
-
786
- //#ENDIF
787
-
788
797
  // mark the future index as identical (yeah, it's dirty, but cheap 👍)
789
798
  // The main reason to do this, is that when a[aEnd] will be reached,
790
799
  // the loop will likely be on the fast path, as identical to b[bEnd].
@@ -832,14 +841,6 @@ const udomdiff = (parentNode, a, b, before) => {
832
841
  while (bStart < index) {
833
842
  let bNode = b[bStart++];
834
843
  parentNode.insertBefore(bNode, node);
835
-
836
- //#IFDEV
837
- if (bNode instanceof NodeGroup)
838
- bNode.verify();
839
- // if (parentNode instanceof ExprPath)
840
- // parentNode.verify();
841
-
842
- //#ENDIF
843
844
  }
844
845
  }
845
846
  // if the effort wasn't good enough, fallback to a replace,
@@ -852,13 +853,6 @@ const udomdiff = (parentNode, a, b, before) => {
852
853
  bNode,
853
854
  aNode
854
855
  );
855
-
856
- //#IFDEV
857
- if (aNode instanceof NodeGroup)
858
- aNode.verify();
859
- // if (parentNode instanceof ExprPath)
860
- // parentNode.verify();
861
- //#ENDIF
862
856
  }
863
857
  }
864
858
  // otherwise move the source forward, 'cause there's nothing to do
@@ -871,13 +865,6 @@ const udomdiff = (parentNode, a, b, before) => {
871
865
  else {
872
866
  let aNode = a[aStart++];
873
867
  parentNode.removeChild(aNode);
874
-
875
- //#IFDEV
876
- if (aNode instanceof NodeGroup)
877
- aNode.verify();
878
- // if (parentNode instanceof ExprPath)
879
- // parentNode.verify();
880
- //#ENDIF
881
868
  }
882
869
  }
883
870
  }
@@ -1005,13 +992,13 @@ class ExprPath {
1005
992
  case 2: // PathType.Multiple:
1006
993
  this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
1007
994
  break;
1008
- case 5: // PathType.Comment:
995
+ case 4: // PathType.Comment:
1009
996
  // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
1010
997
  break;
1011
- case 6: // PathType.Event:
998
+ case 5: // PathType.Event:
1012
999
  this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
1013
1000
  break;
1014
- default: // TODO: Is this still used? Lots of tests fail without it.
1001
+ default: // 3 PathType.Attribute
1015
1002
  // One attribute value may have multiple expressions. Here we apply them all at once.
1016
1003
  this.applyValueAttrib(this.nodeMarker, exprs);
1017
1004
  break;
@@ -1021,7 +1008,7 @@ class ExprPath {
1021
1008
  /**
1022
1009
  * Insert/replace the nodes created by a single expression.
1023
1010
  * Called by applyExprs()
1024
- * This function is recursive, as the functions it calls also call it.
1011
+ * This function is recursive. It calls functions that call applyNodes().
1025
1012
  * @param expr {Expr}
1026
1013
  * @param freeNodeGroups {boolean}
1027
1014
  * @return {Node[]} New Nodes created. */
@@ -1056,7 +1043,6 @@ class ExprPath {
1056
1043
  if (secondPass.length) {
1057
1044
  for (let [nodesIndex, ngIndex] of secondPass) {
1058
1045
  let ng = path.getNodeGroup(newNodes[nodesIndex], false);
1059
-
1060
1046
  let ngNodes = ng.getNodes();
1061
1047
 
1062
1048
  /*#IFDEV*/assert(!(newNodes[nodesIndex] instanceof NodeGroup));/*#ENDIF*/
@@ -1077,11 +1063,8 @@ class ExprPath {
1077
1063
 
1078
1064
  /*#IFDEV*/assert(!path.nodeGroups.includes(null));/*#ENDIF*/
1079
1065
 
1080
-
1081
-
1082
1066
  let oldNodes = path.getNodes();
1083
1067
 
1084
-
1085
1068
  // This pre-check makes it a few percent faster?
1086
1069
  let same = Util.arraySame(oldNodes, newNodes);
1087
1070
  if (!same) {
@@ -1094,7 +1077,6 @@ class ExprPath {
1094
1077
  // Fast clear method
1095
1078
  let isNowEmpty = oldNodes.length && !newNodes.length;
1096
1079
  if (!isNowEmpty || !path.fastClear())
1097
-
1098
1080
  // Rearrange nodes.
1099
1081
  udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
1100
1082
 
@@ -1107,18 +1089,17 @@ class ExprPath {
1107
1089
  ng.removeAndSaveOrphans();
1108
1090
 
1109
1091
  // Instantiate components created within ${...} expressions.
1110
- // Embedded style tags are handled elsewhere, but where?
1092
+ // Also see this.applyExactNodes() which handles calling render() on web components even if they are unchanged.
1111
1093
  for (let el of newNodes) {
1112
- if (el instanceof HTMLElement) {
1094
+ if (el?.nodeType === 1) { // HTMLElement
1113
1095
  if (el.hasAttribute('solarite-placeholder'))
1114
- this.parentNg.instantiateComponent(el);
1096
+ this.parentNg.handleComponent(el, null, true);
1115
1097
  for (let child of el.querySelectorAll('[solarite-placeholder]'))
1116
- this.parentNg.instantiateComponent(child);
1098
+ this.parentNg.handleComponent(child, null, true);
1117
1099
  }
1118
1100
  }
1119
1101
  }
1120
1102
 
1121
-
1122
1103
  /*#IFDEV*/path.verify();/*#ENDIF*/
1123
1104
  }
1124
1105
 
@@ -1228,7 +1209,7 @@ class ExprPath {
1228
1209
  }
1229
1210
 
1230
1211
  // String/Number/Date/Boolean
1231
- else if (!(expr instanceof Template) && !(expr instanceof Node)){
1212
+ else if (!(expr instanceof Template) && !(expr?.nodeType)){
1232
1213
  // Convert expression to a string.
1233
1214
  if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
1234
1215
  expr = '';
@@ -1238,7 +1219,9 @@ class ExprPath {
1238
1219
  // Get the same Template for the same string each time.
1239
1220
  // let template = Globals.stringTemplates[expr];
1240
1221
  // if (!template) {
1222
+
1241
1223
  let template = new Template([expr], []);
1224
+ template.isText = true;
1242
1225
  // Globals.stringTemplates[expr] = template;
1243
1226
  //}
1244
1227
 
@@ -1263,12 +1246,44 @@ class ExprPath {
1263
1246
 
1264
1247
  if (expr instanceof Template) {
1265
1248
  let ng = this.getNodeGroup(expr, true);
1249
+
1266
1250
  if (ng) {
1251
+ let newestNodes = ng.getNodes();
1252
+ newNodes.push(...newestNodes);
1253
+
1254
+ // New!
1255
+ // Re-apply all expressions if there's a web component, so we can pass them to its constructor.
1256
+ // NodeGroup.applyExprs() is used to call applyComponentExprs() on web components that have expression attributes.
1257
+ // For those that don't, we call applyComponentExprs() directly here.
1258
+ // Also see similar code at the end of this.applyNodes() which handles web components being instantiated the first time.
1259
+ let apply = false;
1260
+ for (let el of newestNodes) {
1261
+ if (el?.nodeType === 1) { // HTMLElement
1262
+
1263
+ if (el.tagName.includes('-')) {
1264
+ if (!expr.exprs.find(expr => expr?.nodeMarker === el))
1265
+ this.parentNg.handleComponent(el, null, true);
1266
+ else // Commenting out this "else" causes render() to be called too often, but other UI code fails if it's present.
1267
+ apply = true;
1268
+ }
1269
+ for (let child of el.querySelectorAll('*')) {
1270
+ if (child.tagName.includes('-')) {
1271
+ if (!expr.exprs.find(expr => expr?.nodeMarker === child))
1272
+ this.parentNg.handleComponent(child, null, true);
1273
+ else
1274
+ apply = true;
1275
+ }
1276
+ }
1277
+ }
1278
+ }
1267
1279
 
1268
- // TODO: Track ranges of changed nodes and only pass those to udomdiff?
1269
- // But will that break the swap benchmark?
1270
- newNodes.push(...ng.getNodes());
1280
+ // This calls render() on web components that have expressions as attributes.
1281
+ if (apply)
1282
+ ng.applyExprs(expr.exprs);
1283
+
1271
1284
  this.nodeGroups.push(ng);
1285
+
1286
+ return ng;
1272
1287
  }
1273
1288
 
1274
1289
  // If expression, mark it to be evaluated later in ExprPath.apply() to find partial match.
@@ -1280,10 +1295,10 @@ class ExprPath {
1280
1295
  }
1281
1296
 
1282
1297
  // Node(s) created by an expression.
1283
- else if (expr instanceof Node) {
1298
+ else if (expr?.nodeType) {
1284
1299
 
1285
1300
  // DocumentFragment created by an expression.
1286
- if (expr instanceof DocumentFragment)
1301
+ if (expr?.nodeType === 11) // DocumentFragment
1287
1302
  newNodes.push(...expr.childNodes);
1288
1303
  else
1289
1304
  newNodes.push(expr);
@@ -1359,8 +1374,8 @@ class ExprPath {
1359
1374
  * @param root */
1360
1375
  applyEventAttrib(node, expr, root) {
1361
1376
  /*#IFDEV*/
1362
- assert(this.type === ExprPathType.Event/* || this.type === PathType.Component*/);
1363
- assert(root instanceof HTMLElement);
1377
+ assert(this.type === ExprPathType.Event);
1378
+ assert(root?.nodeType === 1);
1364
1379
  /*#ENDIF*/
1365
1380
 
1366
1381
  let eventName = this.attrName.slice(2); // remove "on-" prefix.
@@ -1440,7 +1455,7 @@ class ExprPath {
1440
1455
  }
1441
1456
 
1442
1457
  /**
1443
- * Handle values, including two-way binding.
1458
+ * Set the value of an attribute. This can be for any attribute, not just attributes named "value".
1444
1459
  * @param node
1445
1460
  * @param exprs */
1446
1461
  // TODO: node is always this.nodeMarker?
@@ -1470,12 +1485,21 @@ class ExprPath {
1470
1485
  }
1471
1486
  else {
1472
1487
  // TODO: should we remove isFalsy, since these are always props?
1473
- let strValue = Util.isFalsy(value) ? '' : value;
1488
+ const strValue = Util.isFalsy(value) ? '' : value;
1474
1489
 
1475
- // If we don't have this condition, when we call render(), the browser will scroll to the currently
1476
- // selected item in a <select> and mess up manually scrolling to a different value.
1477
- if (strValue !== node[this.attrName])
1478
- node[this.attrName] = strValue;
1490
+ // Special case for contenteditable
1491
+ if (this.attrName === 'value' && node.hasAttribute('contenteditable')) {
1492
+ const existingValue = node.innerHTML;
1493
+ if (strValue !== existingValue)
1494
+ node.innerHTML = strValue;
1495
+ }
1496
+ else {
1497
+
1498
+ // If we don't have this condition, when we call render(), the browser will scroll to the currently
1499
+ // selected item in a <select> and mess up manually scrolling to a different value.
1500
+ if (strValue !== node[this.attrName])
1501
+ node[this.attrName] = strValue;
1502
+ }
1479
1503
  }
1480
1504
 
1481
1505
  // TODO: We need to remove any old listeners, like in bindEventAttribute.
@@ -1495,16 +1519,8 @@ class ExprPath {
1495
1519
 
1496
1520
  // Regular attribute
1497
1521
  else {
1498
- // TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
1499
- // Or make it a new PathType.
1500
- //if (this.attrName === 'disabled')
1501
- // debugger;
1502
-
1503
- // hasOwnProperty() checks only the object, not the parents
1504
- // this.attrName in node checks the node and the parents.
1505
- // This version checks the html element it extends from, to see if has a setter set:
1506
- // Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
1507
- //let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
1522
+ // Cache this on ExprPath.isHtmlProperty when Shell creates the props.
1523
+ // Have ExprPath.clone() copy .isHtmlProperty?
1508
1524
  let isProp = this.isHtmlProperty;
1509
1525
  if (isProp === undefined)
1510
1526
  isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
@@ -1514,7 +1530,7 @@ class ExprPath {
1514
1530
  if (!multiple) {
1515
1531
  Globals$1.currentExprPath = this; // Used by watch()
1516
1532
  if (typeof expr === 'function') {
1517
- if (this.type === 4) { // Don't evaluate functions before passing them to components
1533
+ if (this.isComponent) { // Don't evaluate functions before passing them to components
1518
1534
  return
1519
1535
  }
1520
1536
  this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
@@ -1572,6 +1588,13 @@ class ExprPath {
1572
1588
  // since we also prohibit expressions that are a child of textarea.
1573
1589
  if (isProp)
1574
1590
  node[this.attrName] = joinedValue;
1591
+
1592
+ // Allow one-way binding to contenteditable value attribute.
1593
+ // Contenteditables normally don't have a value attribute and have their content set via innerHTML.
1594
+ // Solarite doesn't allow contenteditables to have expressions as their children.
1595
+ else if (node.hasAttribute('contenteditable'))
1596
+ node.innerHTML = joinedValue;
1597
+
1575
1598
  // TODO: Putting an 'else' here would be more performant
1576
1599
  node.setAttribute(this.attrName, joinedValue);
1577
1600
  }
@@ -1602,11 +1625,9 @@ class ExprPath {
1602
1625
  nodeBefore = childNodes[this.nodeBeforeIndex];
1603
1626
 
1604
1627
  let result = new ExprPath(nodeBefore, nodeMarker, this.type, this.attrName, this.attrValue);
1628
+ result.isComponent = this.isComponent;
1605
1629
 
1606
1630
  //#IFDEV
1607
- result.nodeMarker.exprPath = result;
1608
- if (result.nodeBefore)
1609
- result.nodeBefore.prevExprPath = result;
1610
1631
  result.verify();
1611
1632
  //#ENDIF
1612
1633
 
@@ -1615,9 +1636,7 @@ class ExprPath {
1615
1636
 
1616
1637
  /**
1617
1638
  * Clear the nodeCache of this ExprPath, as well as all parent and child ExprPaths that
1618
- * share the same DOM parent node.
1619
- *
1620
- * TODO: Is recursive clearing ever necessary? */
1639
+ * share the same DOM parent node. */
1621
1640
  clearNodesCache() {
1622
1641
  let path = this;
1623
1642
 
@@ -1630,9 +1649,6 @@ class ExprPath {
1630
1649
  // If stuck in an infinite loop here, the problem is likely due to Template hash colisions.
1631
1650
  // Which cause one path to be the descendant of itself, creating a cycle.
1632
1651
  }
1633
-
1634
- // Commented out on Sep 30, 2024 b/c it was making the benchmark never finish when adding 10k rows.
1635
- //clearChildNodeCache(this);
1636
1652
  }
1637
1653
 
1638
1654
 
@@ -1674,7 +1690,7 @@ class ExprPath {
1674
1690
  // result2.push(...ng.getNodes())
1675
1691
  // return result2;
1676
1692
 
1677
- if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
1693
+ if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple) {
1678
1694
  return [this.nodeMarker];
1679
1695
  }
1680
1696
 
@@ -1734,11 +1750,13 @@ class ExprPath {
1734
1750
  result = collection.deleteAny(template.getExactKey());
1735
1751
  }
1736
1752
 
1737
- if (result) // also delete the matching close key.
1753
+ if (result) {// also delete the matching close key.
1738
1754
  collection.deleteSpecific(template.getCloseKey(), result);
1739
- else {
1740
- return null;
1755
+
1756
+ //result.applyExprs(template.exprs);
1741
1757
  }
1758
+ else
1759
+ return null;
1742
1760
  }
1743
1761
 
1744
1762
  // Find a close match.
@@ -1772,12 +1790,6 @@ class ExprPath {
1772
1790
  return result;
1773
1791
  }
1774
1792
 
1775
- isComponent() {
1776
- // Events won't have type===Component.
1777
- // TODO: Have a special flag for components instead of it being on the type?
1778
- return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
1779
- }
1780
-
1781
1793
  /**
1782
1794
  * TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
1783
1795
  * Nodes that have been used during the current render().
@@ -1835,7 +1847,7 @@ class ExprPath {
1835
1847
  `parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
1836
1848
  'nodes:',
1837
1849
  ...setIndent(this.getNodes().map(item => {
1838
- if (item instanceof Node)
1850
+ if (item?.nodeType)
1839
1851
  return item.outerHTML || item.textContent
1840
1852
  else if (item instanceof NodeGroup)
1841
1853
  return item.debug
@@ -1892,7 +1904,7 @@ class ExprPath {
1892
1904
  /** @enum {int} */
1893
1905
  const ExprPathType = {
1894
1906
  /** Child of a node */
1895
- Content: 1,
1907
+ Content: 1, // TODO: Rename to Nodes
1896
1908
 
1897
1909
  /** One or more whole attributes */
1898
1910
  AttribMultiple: 2,
@@ -1900,14 +1912,11 @@ const ExprPathType = {
1900
1912
  /** Value of an attribute. */
1901
1913
  AttribValue: 3,
1902
1914
 
1903
- /** Value of an attribute being passed to a component. */
1904
- ComponentAttribValue: 4,
1905
-
1906
1915
  /** Expressions inside Html comments. */
1907
- Comment: 5,
1916
+ Comment: 4,
1908
1917
 
1909
1918
  /** Value of an attribute. */
1910
- Event: 6,
1919
+ Event: 5,
1911
1920
  };
1912
1921
 
1913
1922
 
@@ -2075,7 +2084,7 @@ class Shell {
2075
2084
  //#ENDIF
2076
2085
 
2077
2086
  if (html.length === 1 && !html[0].match(/[<&]/)) {
2078
- this.fragment = document.createTextNode(html[0]);
2087
+ this.fragment = Globals$1.doc.createTextNode(html[0]);
2079
2088
  return;
2080
2089
  }
2081
2090
 
@@ -2083,18 +2092,18 @@ class Shell {
2083
2092
  // 1. Add placeholders
2084
2093
  let joinedHtml = Shell.addPlaceholders(html);
2085
2094
 
2086
- let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
2095
+ let template = Globals$1.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
2087
2096
  if (joinedHtml)
2088
2097
  template.innerHTML = joinedHtml;
2089
2098
  else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
2090
- template.content.append(document.createTextNode(''));
2099
+ template.content.append(Globals$1.doc.createTextNode(''));
2091
2100
  this.fragment = template.content;
2092
2101
 
2093
2102
  // 2. Find placeholders
2094
2103
  let node;
2095
2104
  let toRemove = [];
2096
2105
  let placeholdersUsed = 0;
2097
- const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
2106
+ const walker = Globals$1.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
2098
2107
  while (node = walker.nextNode()) {
2099
2108
 
2100
2109
  // Remove previous after each iteration, so paths will still be calculated correctly.
@@ -2130,10 +2139,13 @@ class Shell {
2130
2139
  // Replace comment placeholders
2131
2140
  else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
2132
2141
 
2142
+ if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
2143
+ throw new Error(`Contenteditable can't have expressions inside them. Use <div contenteditable value="\${...}"> instead.`);
2144
+
2133
2145
  // Get or create nodeBefore.
2134
2146
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
2135
2147
  if (!nodeBefore) {
2136
- nodeBefore = document.createComment('ExprPath:'+this.paths.length);
2148
+ nodeBefore = Globals$1.doc.createComment('ExprPath:'+this.paths.length);
2137
2149
  node.parentNode.insertBefore(nodeBefore, node);
2138
2150
  }
2139
2151
  /*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
@@ -2158,9 +2170,9 @@ class Shell {
2158
2170
  placeholdersUsed ++;
2159
2171
  }
2160
2172
 
2173
+ // Comments become text nodes when inside textareas.
2161
2174
  else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
2162
2175
  throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
2163
-
2164
2176
 
2165
2177
 
2166
2178
  // Sometimes users will comment out a block of html code that has expressions.
@@ -2184,7 +2196,7 @@ class Shell {
2184
2196
 
2185
2197
  let placeholders = [];
2186
2198
  for (let i = 0; i<parts.length; i++) {
2187
- let current = document.createTextNode(parts[i]);
2199
+ let current = Globals$1.doc.createTextNode(parts[i]);
2188
2200
  node.parentNode.insertBefore(current, node);
2189
2201
  if (i > 0)
2190
2202
  placeholders.push(current);
@@ -2223,9 +2235,9 @@ class Shell {
2223
2235
  path.nodeMarkerPath = getNodePath(path.nodeMarker);
2224
2236
 
2225
2237
  // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
2226
- if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
2238
+ if ((path.type === ExprPathType.AttribValue || path.type === ExprPathType.Event) && path.nodeMarker.nodeType === 1 &&
2227
2239
  (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
2228
- path.type = ExprPathType.ComponentAttribValue;
2240
+ path.isComponent = true;
2229
2241
  }
2230
2242
  }
2231
2243
 
@@ -2399,30 +2411,39 @@ class NodeGroup {
2399
2411
  * @type {?Map<HTMLStyleElement, string>} */
2400
2412
  styles;
2401
2413
 
2414
+ dynamicComponents = new Set();
2415
+ staticComponents = [];
2416
+
2417
+ /** @type {Template} */
2418
+ template;
2419
+
2402
2420
 
2403
2421
  /**
2404
2422
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
2405
2423
  * @param template {Template} Create it from the html strings and expressions in this template.
2406
2424
  * @param parentPath {?ExprPath} */
2407
2425
  constructor(template, parentPath=null) {
2426
+ this.rootNg = parentPath?.parentNg?.rootNg || this;
2427
+ this.parentPath = parentPath;
2428
+
2408
2429
  if (!(this instanceof RootNodeGroup)) {
2409
2430
 
2410
- let [fragment, shell] = this.init(template, parentPath);
2431
+ let [fragment, shell] = this.populateFromTemplate(template);
2411
2432
 
2412
2433
  if (fragment && template.exprs.length) {
2413
2434
  this.updatePaths(fragment, shell.paths);
2414
2435
 
2415
2436
  // Static web components can sometimes have children created via expressions.
2416
2437
  // But calling applyExprs() will mess up the shell's path to them.
2417
- // So we find them first, then call activateStaticComponents() after their children have been created.
2418
- let staticComponents = this.findStaticComponents(fragment, shell);
2438
+ // So we find them first, then call instantiateStaticComponents() after their children have been created.
2439
+ this.staticComponents = this.findStaticComponents(fragment, shell);
2419
2440
 
2420
2441
  this.activateEmbeds(fragment, shell);
2421
2442
 
2422
2443
  // Apply exprs
2423
2444
  this.applyExprs(template.exprs);
2424
2445
 
2425
- this.instantiateStaticComponents(staticComponents);
2446
+ this.instantiateStaticComponents(this.staticComponents);
2426
2447
  }
2427
2448
  else if (shell)
2428
2449
  this.activateEmbeds(fragment, shell);
@@ -2433,47 +2454,33 @@ class NodeGroup {
2433
2454
  * Common init shared by RootNodeGroup and NodeGroup constructors.
2434
2455
  * But in a separate function because they need to do this at a different step.
2435
2456
  * @param template {Template} Create it from the html strings and expressions in this template.
2436
- * @param parentPath {?ExprPath}
2437
- * @param exactKey {?string} Optional, if already calculated.
2438
- * @param closeKey {?string}
2439
- * @returns {[DocumentFragment, Shell]} */
2440
- init(template, parentPath=null, exactKey=null, closeKey=null) {
2441
- this.exactKey = exactKey || template.getExactKey();
2442
- this.closeKey = closeKey || template.getCloseKey();
2443
-
2444
- this.parentPath = parentPath;
2445
- this.rootNg = parentPath?.parentNg?.rootNg || this;
2446
-
2457
+ * @returns {[DocumentFragment, Shell]} The Shell created from the template,a nd the fragment cloned from the Shell.*/
2458
+ populateFromTemplate(template) {
2447
2459
  /*#IFDEV*/assert(this.rootNg);/*#ENDIF*/
2448
-
2449
- /** @type {Template} */
2450
2460
  this.template = template;
2451
-
2452
- // new! Is this needed?
2453
- template.nodeGroup = this;
2454
-
2455
- // Get a cached version of the parsed and instantiated html, and ExprPaths.
2461
+ this.exactKey = template.getExactKey();
2462
+ this.closeKey = template.getCloseKey();
2456
2463
 
2457
2464
  // If it's just a text node, skip a bunch of unnecessary steps.
2458
- if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
2459
- //let doc = this.rootNg.startNode?.ownerDocument || document;
2460
- let textNode = document.createTextNode(template.html[0]);
2461
-
2465
+ if (template.isText) {
2466
+ let textNode = Globals$1.doc.createTextNode(template.html[0]);
2462
2467
  this.startNode = this.endNode = textNode;
2463
2468
  return [];
2464
2469
  }
2470
+
2471
+ // Get a cached version of the parsed and instantiated html, and ExprPaths:
2465
2472
  else {
2466
2473
  let shell = Shell.get(template.html);
2467
2474
  let fragment = shell.fragment.cloneNode(true);
2468
2475
 
2469
- if (fragment instanceof DocumentFragment) {
2476
+ if (fragment?.nodeType === 11) { // DocumentFragment
2470
2477
  let childNodes = fragment.childNodes;
2471
2478
  this.startNode = childNodes[0];
2472
2479
  this.endNode = childNodes[childNodes.length - 1];
2473
2480
  }
2474
- else {
2481
+ else
2475
2482
  this.startNode = this.endNode = fragment;
2476
- }
2483
+
2477
2484
  return [fragment, shell];
2478
2485
  }
2479
2486
  }
@@ -2512,6 +2519,7 @@ class NodeGroup {
2512
2519
  exprIndex--;
2513
2520
  }
2514
2521
 
2522
+
2515
2523
  // TODO: Need to end and restart this block when going from one component to the next?
2516
2524
  // Think of having two adjacent components.
2517
2525
  // But the dynamicAttribsAdjacet test already passes.
@@ -2520,11 +2528,11 @@ class NodeGroup {
2520
2528
  // 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
2521
2529
  // 2. Otherwise send them to its render function.
2522
2530
  // Components with no expressions as attributes are instead activated in activateEmbeds().
2523
- if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
2531
+ if (path.nodeMarker !== this.rootNg.root && path.isComponent) {
2524
2532
 
2525
- if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
2533
+ if (!nextPath || !nextPath.isComponent || nextPath.nodeMarker !== path.nodeMarker)
2526
2534
  lastComponentPathIndex = i;
2527
- let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
2535
+ let isFirstComponentPath = !prevPath || !prevPath.isComponent || prevPath.nodeMarker !== path.nodeMarker;
2528
2536
 
2529
2537
  if (isFirstComponentPath) {
2530
2538
 
@@ -2534,7 +2542,7 @@ class NodeGroup {
2534
2542
  componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
2535
2543
  }
2536
2544
 
2537
- this.applyComponentExprs(path.nodeMarker, componentProps);
2545
+ this.handleComponent(path.nodeMarker, componentProps, true);
2538
2546
 
2539
2547
  // Set attributes on component.
2540
2548
  for (let j=i; j<=lastComponentPathIndex; j++)
@@ -2553,7 +2561,10 @@ class NodeGroup {
2553
2561
  // TODO: Only do this if we have ExprPaths within styles?
2554
2562
  this.updateStyles();
2555
2563
 
2556
-
2564
+ // Call render() on static web components. This makes the component.staticAttribs() test work.
2565
+ for (let el of this.staticComponents)
2566
+ if (el.render)
2567
+ el.render(Util.attribsToObject(el)); // It has no expressions.
2557
2568
 
2558
2569
  // Invalidate the nodes cache because we just changed it.
2559
2570
  this.nodesCache = null;
@@ -2569,50 +2580,39 @@ class NodeGroup {
2569
2580
  }
2570
2581
 
2571
2582
  /**
2572
- * Create a nested Component or call render with the new props.
2573
- * @param el {Solarite:HTMLElement}
2574
- * @param props {Object} */
2575
- applyComponentExprs(el, props) {
2576
-
2577
- // TODO: Does a hash of this already exist somewhere?
2578
- // Perhaps if Components were treated as child NodeGroups, which would need to be the child of an ExprPath,
2579
- // then we could re-use the hash and logic from NodeManager?
2580
- let newHash = getObjectHash(props);
2581
-
2583
+ * Unified path to ensure a child component is instantiated (if placeholder) and optionally rendered.
2584
+ * @param el {HTMLElement}
2585
+ * @param props {?Object}
2586
+ * @param doRender {boolean}
2587
+ * @return {HTMLElement} The (possibly replaced) element. */
2588
+ handleComponent(el, props=null, doRender=true) {
2582
2589
  let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
2583
2590
  let isPreIsElement = el.hasAttribute('_is');
2584
-
2585
-
2586
- // Instantiate a placeholder.
2591
+ let attribs, children;
2587
2592
  if (isPreHtmlElement || isPreIsElement)
2588
- el = this.instantiateComponent(el, isPreHtmlElement, props);
2589
-
2590
- // Call render() with the same params that would've been passed to the constructor.
2591
- // We do this even if the arguments haven't changed, so we can let the child component
2592
- // compare the arguments and then decide for itself whether it wants to re-render.
2593
- else if (el.render) {
2594
- //let oldHash = Globals.componentArgsHash.get(el);
2595
- //if (oldHash !== newHash) { // Only if not changed.
2596
- let args = {};
2593
+ [el, attribs, children] = this.instantiateComponent(el, isPreHtmlElement, props);
2594
+ if (doRender && el.render) {
2595
+ if (!attribs) {
2596
+ attribs = Util.attribsToObject(el);
2597
2597
  for (let name in props || {})
2598
- args[Util.dashesToCamel(name)] = props[name];
2599
- el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
2600
- //}
2598
+ attribs[Util.dashesToCamel(name)] = props[name];
2599
+ children = el.childNodes;
2600
+ }
2601
+ el.render(attribs, children);
2601
2602
  }
2602
-
2603
- Globals$1.componentArgsHash.set(el, newHash);
2603
+ return el;
2604
2604
  }
2605
-
2605
+
2606
2606
  /**
2607
2607
  * We swap the placeholder element for the real element so we can pass its dynamic attributes
2608
2608
  * to its constructor.
2609
+ * This is only called by handleComponent()
2610
+ * This does not call render()
2609
2611
  *
2610
- * The logic of this function is complex and could use cleaning up.
2611
- *
2612
- * @param el
2612
+ * @param el {HTMLElement}
2613
2613
  * @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
2614
2614
  * @param props {Object} Attributes with dynamic values.
2615
- * @return {HTMLElement} */
2615
+ * @return {[HTMLElement, attribs:Object, children:Node[]]}} */
2616
2616
  instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
2617
2617
  if (isPreHtmlElement === undefined)
2618
2618
  isPreHtmlElement = !el.hasAttribute('_is');
@@ -2627,19 +2627,12 @@ class NodeGroup {
2627
2627
  if (!Constructor)
2628
2628
  throw new Error(`The custom tag name ${tagName} is not registered.`)
2629
2629
 
2630
- let attribs = {};
2630
+ // Pass other attribs to constructor, since otherwise they're not yet set on the element,
2631
+ // and the constructor would otherwise have no way to see them.
2632
+ let attribs = Util.attribsToObject(el, 'solarite-placeholder');
2631
2633
  for (let name in props || {})
2632
2634
  attribs[Util.dashesToCamel(name)] = props[name];
2633
2635
 
2634
- // Pass other attribs to constructor, since otherwise they're not yet set on the element,
2635
- // and the constructor would otherwise have no way to see them.
2636
- if (el.attributes.length) {
2637
- for (let attrib of el.attributes) {
2638
- let attribName = Util.dashesToCamel(attrib.name);
2639
- if (!attribs.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
2640
- attribs[attribName] = attrib.value;
2641
- }
2642
- }
2643
2636
 
2644
2637
  // Create the web component.
2645
2638
  // Get the children that aren't Solarite's comment placeholders.
@@ -2670,7 +2663,7 @@ class NodeGroup {
2670
2663
  if (this.endNode === el)
2671
2664
  this.endNode = newEl;
2672
2665
 
2673
-
2666
+ // This is used only if inheriting from the Solarite class.
2674
2667
  // applyComponentExprs() is called because we're rendering.
2675
2668
  // So we want to render the sub-component also.
2676
2669
  if (newEl.renderFirstTime)
@@ -2694,7 +2687,7 @@ class NodeGroup {
2694
2687
  newEl.setAttribute(name, val);
2695
2688
  }
2696
2689
 
2697
- return newEl;
2690
+ return [newEl, attribs, children];
2698
2691
  }
2699
2692
 
2700
2693
  /**
@@ -2741,18 +2734,21 @@ class NodeGroup {
2741
2734
  * Requires the nodeCache to be present. */
2742
2735
  removeAndSaveOrphans() {
2743
2736
  /*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
2744
- let fragment = document.createDocumentFragment();
2737
+ let fragment = Globals$1.doc.createDocumentFragment();
2745
2738
  for (let node of this.getNodes())
2746
2739
  fragment.append(node);
2747
2740
  }
2748
2741
 
2749
2742
 
2750
- updatePaths(fragment, paths, offset) {
2751
- // Update paths to point to the fragment.
2743
+ /**
2744
+ * @param fragment {DocumentFragment}
2745
+ * @param paths
2746
+ * @param startingPathDepth {int} */
2747
+ updatePaths(fragment, paths, startingPathDepth) {
2752
2748
  let pathLength = paths.length;
2753
2749
  this.paths.length = pathLength;
2754
2750
  for (let i=0; i<pathLength; i++) {
2755
- let path = paths[i].clone(fragment, offset);
2751
+ let path = paths[i].clone(fragment, startingPathDepth);
2756
2752
  path.parentNg = this;
2757
2753
  this.paths[i] = path;
2758
2754
  }
@@ -2779,7 +2775,7 @@ class NodeGroup {
2779
2775
  `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
2780
2776
  'nodes:',
2781
2777
  ...setIndent(this.getNodes().map(item => {
2782
- if (item instanceof Node) {
2778
+ if (item?.nodeType) {
2783
2779
 
2784
2780
  let tree = nodeToArrayTree(item, nextNode => {
2785
2781
 
@@ -2834,7 +2830,7 @@ class NodeGroup {
2834
2830
  }
2835
2831
  //#ENDIF
2836
2832
 
2837
- findStaticComponents(root, shell, pathOffset=0) {
2833
+ findStaticComponents(root, shell, startingPathDepth=0) {
2838
2834
  let result = [];
2839
2835
 
2840
2836
  // static components. These are WebComponents that do not have any constructor arguments that are expressions.
@@ -2842,8 +2838,8 @@ class NodeGroup {
2842
2838
  // Maybe someday these two paths will be merged?
2843
2839
  // Must happen before ids because instantiateComponent will replace the element.
2844
2840
  for (let path of shell.staticComponents) {
2845
- if (pathOffset)
2846
- path = path.slice(0, -pathOffset);
2841
+ if (startingPathDepth)
2842
+ path = path.slice(0, -startingPathDepth);
2847
2843
  let el = resolveNodePath(root, path);
2848
2844
 
2849
2845
  // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
@@ -2855,8 +2851,9 @@ class NodeGroup {
2855
2851
  }
2856
2852
 
2857
2853
  instantiateStaticComponents(staticComponents) {
2858
- for (let el of staticComponents)
2859
- this.instantiateComponent(el);
2854
+ // TODO: Why do we not call render() on the static component here? The tests pass either way.
2855
+ for (let i in staticComponents)
2856
+ staticComponents[i] = this.handleComponent(staticComponents[i], null, false);
2860
2857
  }
2861
2858
 
2862
2859
  /**
@@ -2877,7 +2874,7 @@ class NodeGroup {
2877
2874
  let el = resolveNodePath(root, path);
2878
2875
  Util.bindId(rootEl, el);
2879
2876
  }
2880
- }
2877
+ }
2881
2878
 
2882
2879
  // styles
2883
2880
  if (options?.styles !== false) {
@@ -2907,9 +2904,8 @@ class NodeGroup {
2907
2904
  }
2908
2905
  }
2909
2906
  }
2910
- }
2911
-
2912
-
2907
+ }
2908
+
2913
2909
  class RootNodeGroup extends NodeGroup {
2914
2910
 
2915
2911
  /**
@@ -2923,18 +2919,18 @@ class RootNodeGroup extends NodeGroup {
2923
2919
  exprsToRender = new Map();
2924
2920
 
2925
2921
  /**
2926
- *
2927
- * @param template
2928
- * @param el
2929
- * @param options {?object}
2930
- */
2922
+ * @param template {Template}
2923
+ * @param el {?HTMLElement} Optional, pre-existing htmlElement tat will be the root.
2924
+ * @param options {?object} */
2931
2925
  constructor(template, el, options) {
2932
2926
  super(template);
2933
2927
 
2934
2928
  this.options = options;
2935
2929
 
2936
- this.rootNg = this;
2937
- let [fragment, shell] = this.init(template);
2930
+ let [fragment, shell] = this.populateFromTemplate(template);
2931
+
2932
+ let startingPathDepth = 0;
2933
+
2938
2934
 
2939
2935
  if (fragment instanceof Text) {
2940
2936
 
@@ -2945,25 +2941,25 @@ class RootNodeGroup extends NodeGroup {
2945
2941
  el.append(fragment);
2946
2942
  this.root = el;
2947
2943
  }
2944
+ else
2945
+ throw new Error('Cannot create a standalone text node');
2948
2946
  Globals$1.nodeGroups.set(this.root, this);
2949
2947
  }
2948
+
2949
+
2950
2950
  else {
2951
2951
 
2952
2952
  // If adding NodeGroup to an element.
2953
- let offset = 0;
2954
- let root = fragment; // TODO: Rename so it's not confused with this.root.
2955
2953
  if (el) {
2956
- Globals$1.nodeGroups.set(el, this);
2954
+ this.root = el;
2957
2955
 
2958
2956
  // Save slot children
2959
2957
  let slotChildren;
2960
2958
  if (el.childNodes.length) {
2961
- slotChildren = document.createDocumentFragment();
2959
+ slotChildren = Globals$1.doc.createDocumentFragment();
2962
2960
  slotChildren.append(...el.childNodes);
2963
2961
  }
2964
2962
 
2965
- this.root = el;
2966
-
2967
2963
  // If el should replace the root node of the fragment.
2968
2964
  if (isReplaceEl(fragment, el)) {
2969
2965
  el.append(...fragment.children[0].childNodes);
@@ -2974,8 +2970,10 @@ class RootNodeGroup extends NodeGroup {
2974
2970
  el.setAttribute(attrib.name, attrib.value);
2975
2971
 
2976
2972
  // Go one level deeper into all of shell's paths.
2977
- offset = 1;
2978
- } else {
2973
+ startingPathDepth = 1;
2974
+ }
2975
+
2976
+ else {
2979
2977
  let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2980
2978
  if (!isEmpty)
2981
2979
  el.append(...fragment.childNodes);
@@ -3003,35 +3001,35 @@ class RootNodeGroup extends NodeGroup {
3003
3001
  el.append(slotChildren);
3004
3002
  }
3005
3003
 
3006
- root = el;
3007
-
3008
3004
  this.startNode = el;
3009
3005
  this.endNode = el;
3010
- } else {
3006
+ }
3007
+
3008
+ // Instantiate as a standalone element.
3009
+ else {
3011
3010
  let singleEl = getSingleEl(fragment);
3012
- this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
3011
+ this.root = singleEl || fragment; // We return the whole fragment when calling h() with a collection of nodes.
3013
3012
 
3014
- Globals$1.nodeGroups.set(this.root, this);
3015
- if (singleEl) {
3016
- root = singleEl;
3017
- offset = 1;
3018
- }
3013
+ if (singleEl)
3014
+ startingPathDepth = 1;
3019
3015
  }
3020
-
3021
- this.updatePaths(root, shell.paths, offset);
3016
+ Globals$1.nodeGroups.set(this.root, this);
3017
+ this.updatePaths(this.root, shell.paths, startingPathDepth);
3022
3018
 
3023
3019
  // Static web components can sometimes have children created via expressions.
3024
3020
  // But calling applyExprs() will mess up the shell's path to them.
3025
3021
  // So we find them first, then call activateStaticComponents() after their children have been created.
3026
- let staticComponents = this.findStaticComponents(root, shell, offset);
3022
+ this.staticComponents = this.findStaticComponents(this.root, shell, startingPathDepth);
3027
3023
 
3028
- this.activateEmbeds(root, shell, offset);
3024
+ this.activateEmbeds(this.root, shell, startingPathDepth);
3029
3025
 
3030
3026
  // Apply exprs
3031
3027
  this.applyExprs(template.exprs);
3032
3028
 
3033
- this.instantiateStaticComponents(staticComponents);
3029
+ this.instantiateStaticComponents(this.staticComponents);
3034
3030
  }
3031
+
3032
+
3035
3033
  }
3036
3034
  }
3037
3035
 
@@ -3073,8 +3071,7 @@ class Template {
3073
3071
  /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
3074
3072
  hashedFields;
3075
3073
 
3076
- /** @type {NodeGroup} */
3077
- nodeGroup;
3074
+ isText;
3078
3075
 
3079
3076
  /**
3080
3077
  *
@@ -3179,169 +3176,203 @@ class Template {
3179
3176
 
3180
3177
  return this.closeKey;
3181
3178
  }
3182
- }
3183
3179
 
3180
+ /**
3181
+ * @param tag {string}
3182
+ * @param props {?Record<string, any>}
3183
+ * @param children
3184
+ * @returns {Template} */
3185
+ static fromJsx(tag, props, children) {
3184
3186
 
3185
- /**
3186
- * @typedef {Object} RenderOptions
3187
- * @property {boolean=} styles - Replace :host in style tags to scope them locally.
3188
- * @property {boolean=} scripts - Execute script tags.
3189
- * @property {boolean=} ids - Create references to elements with id or data-id attributes.
3190
- * @property {?boolean} render - Deprecated.
3191
- * Used only when options are given to a class super constructor inheriting from Solarite.
3192
- * True to call render() immediately in super constructor.
3193
- * False to automatically call render() at all.
3194
- * Undefined (default) to call render() when added to the DOM, unless already rendered.
3195
- */
3196
-
3197
- /**
3198
- * Convert strings to HTMLNodes.
3199
- * Using h`...` as a tag will always create a Template.
3200
- * Using h() as a function() will always create a DOM element.
3201
- *
3202
- * Features beyond what standard js tagged template strings do:
3203
- * 1. r`` sub-expressions
3204
- * 2. functions, nodes, and arrays of nodes as sub-expressions.
3205
- * 3. html-escape all expressions by default, unless wrapped in r()
3206
- * 4. event binding
3207
- * 5. TODO: list more
3208
- *
3209
- * Currently supported:
3210
- * 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
3211
- * 2. h(el, template, ?options) // Render the Template created by #1 to element.
3212
- *
3213
- * 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
3214
- *
3215
- * 4. h('Hello'); // Create single text node.
3216
- * 5. h('<b>Hello</b>'); // Create single HTMLElement
3217
- * 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3218
- * 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
3219
- * // includes properly handling nested components and r`` sub-expressions.
3220
- * 8. h(template) // Render Template created by #1.
3221
- *
3222
- * 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3223
- * 10. h(string, object, ...) // JSX TODO
3224
- * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
3225
- * @param exprs {*[]|string|Template|Object}
3226
- * @return {Node|HTMLElement|Template} */
3227
- function h(htmlStrings=undefined, ...exprs) {
3187
+ // HTML void elements that must not have closing tags
3188
+ const isVoid = selfClosingTags.has(tag.toLowerCase());
3228
3189
 
3229
- if (htmlStrings === undefined && !exprs.length && arguments.length)
3230
- throw new Error('h() cannot be called with undefined.');
3190
+ // Build htmlStrings/exprs so Shell can place placeholders in attribute values and child content.
3191
+ let htmlStrings = [];
3192
+ let templateExprs = [];
3231
3193
 
3232
- // TODO: Make this a more flat if/else and call other functions for the logic.
3233
- if (htmlStrings instanceof Node) {
3234
- let parent = htmlStrings, template = exprs[0];
3194
+ // Opening tag
3195
+ let open = `<${tag}`;
3235
3196
 
3236
- // 1
3237
- if (!(exprs[0] instanceof Template)) {
3238
- if (parent.shadowRoot)
3239
- parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
3197
+ // Attributes
3198
+ if (props && typeof props === 'object') {
3199
+ for (let name in props) {
3200
+ let value = props[name];
3240
3201
 
3241
- let options = exprs[0];
3202
+ // id and data-id are static in templates — never expressions
3203
+ if (name === 'id' || name === 'data-id') {
3204
+ // Write directly into the opening string with quotes
3205
+ open += ` ${name}="${value}"`;
3206
+ continue;
3207
+ }
3242
3208
 
3243
- // Return a tagged template function that applies the tagged themplate to parent.
3244
- let taggedTemplate = (htmlStrings, ...exprs) => {
3245
- Globals$1.rendered.add(parent);
3246
- let template = new Template(htmlStrings, exprs);
3247
- return template.render(parent, options);
3248
- };
3249
- return taggedTemplate;
3209
+ // Dynamic attribute value: functions are unquoted (e.g., onclick=${fn}), others quoted
3210
+ if (typeof value === 'function') {
3211
+ open += ` ${name}=`;
3212
+ htmlStrings.push(open);
3213
+ templateExprs.push(value);
3214
+ // reset so subsequent attributes start fresh (e.g., ' title=')
3215
+ open = ``;
3216
+ }
3217
+ else {
3218
+ open += ` ${name}=`;
3219
+ htmlStrings.push(open);
3220
+ templateExprs.push(value);
3221
+ // reset so subsequent attributes start fresh (e.g., ' title=')
3222
+ open = ``;
3223
+ }
3224
+ }
3250
3225
  }
3251
3226
 
3252
- // 2. Render template created by #4 to element.
3253
- else { // instanceof Template
3254
- let options = exprs[1];
3255
- template.render(parent, options);
3256
-
3257
- // Append on the first go.
3258
- if (!parent.childNodes.length && this) {
3259
- // TODO: Is this ever executed?
3260
- debugger;
3261
- parent.append(this.rootNg.getParentNode());
3227
+ // Finalize opening tag precisely to match tagged template splitting
3228
+ if (!isVoid) {
3229
+ const pushedAny = htmlStrings.length > 0;
3230
+ // If nothing pushed yet (no dynamic attrs), push the entire open + '>'
3231
+ if (!pushedAny)
3232
+ htmlStrings.push(open + '>');
3233
+ else {
3234
+ // If we were in a quoted attr (open === '"'), then the string after expr is '">' ;
3235
+ // Otherwise (function-valued attr), the string after expr is just '>'
3236
+ htmlStrings.push(open === '"' ? '">' : '>');
3262
3237
  }
3238
+
3239
+ for (let child of children)
3240
+ addChild(child, htmlStrings, templateExprs);
3241
+ }
3242
+
3243
+ // Closing tag (not for void tags)
3244
+ if (!isVoid) {
3245
+ // If we never emitted the '>' for the open tag (no children were added),
3246
+ // then it was appended above before children. Now just add the closing tag to the last html segment.
3247
+ let lastIdx = htmlStrings.length - 1;
3248
+ htmlStrings[lastIdx] += `</${tag}>`;
3249
+ }
3250
+ else {
3251
+ // Void element: ensure we emitted a trailing '>' segment
3252
+ const pushedAny = htmlStrings.length > 0;
3253
+ if (!pushedAny)
3254
+ htmlStrings.push(open + '>');
3255
+ else
3256
+ htmlStrings.push('>');
3263
3257
  }
3264
- }
3265
3258
 
3266
- // 3. Path if used as a template tag.
3267
- else if (Array.isArray(htmlStrings)) {
3268
- return new Template(htmlStrings, exprs);
3259
+ // Ensure invariant
3260
+ //assert(htmlStrings.length === templateExprs.length + 1);
3261
+ //console.log([htmlStrings, templateExprs])
3262
+ return new Template(htmlStrings, templateExprs);
3269
3263
  }
3264
+ }
3270
3265
 
3271
- else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
3272
- // 10. JSX
3273
- if (typeof exprs[0] === 'object') {
3274
- exprs[0] || {};
3275
- exprs.slice(1);
3276
3266
 
3277
- let templateHtmlStrings = [];
3278
- let templateExprs = [];
3267
+ const selfClosingTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
3279
3268
 
3280
- // TODO How to know which children are static html and which are expression placeholders?
3281
- // Perhaps we have to treat every text child as a string?
3282
3269
 
3283
- assert(templateHtmlStrings.length === templateExprs.length+1);
3284
- return new Template(templateHtmlStrings, templateExprs);
3270
+ /**
3271
+ * Add child Templates that were already created via h() and Template.fromJsx()
3272
+ * @param template {Template}
3273
+ * @param html {string[]}
3274
+ * @param exprs {any[]} */
3275
+ const addChild = (template, html, exprs) => {
3276
+
3277
+ if (Array.isArray(template)) {
3278
+ for (let c of template)
3279
+ addChild(c, html, exprs);
3280
+ }
3281
+ else {
3282
+ let flatten = false;
3283
+ if (template instanceof Template) {
3284
+ // Heuristic to match tagged-template splitting:
3285
+ // - Flatten if the child has expressions (so JSX can inline attribute/value placeholders like tagged literals would).
3286
+ // - Also flatten void elements (e.g., <img>) so they inline like literals.
3287
+ // - Otherwise, keep as a dynamic child placeholder to match cases where the tagged template used an expression child.
3288
+ const childHasExprs = template.exprs.length > 0;
3289
+ if (childHasExprs)
3290
+ flatten = true;
3291
+ else {
3292
+ const m = (template.html[0] || '').match(/^<([a-zA-Z][\w:-]*)/);
3293
+ const childTag = m ? m[1].toLowerCase() : '';
3294
+ flatten = selfClosingTags.has(childTag);
3295
+ }
3285
3296
  }
3286
3297
 
3298
+ if (flatten) {
3299
+ // Flatten/interleave into current segment to match tagged template splitting
3300
+ html[html.length - 1] += template.html[0];
3301
+ for (let i = 0; i < template.exprs.length; i++) {
3302
+ exprs.push(template.exprs[i]);
3303
+ html.push(template.html[i + 1] ?? '');
3304
+ }
3305
+ } else {
3306
+ // Keep as dynamic child
3307
+ exprs.push(template);
3308
+ html.push('');
3309
+ }
3310
+ }
3311
+ };
3287
3312
 
3288
- // If it starts with a string, trim both ends.
3289
- // TODO: Also trim if it ends with whitespace?
3290
- if (htmlStrings.match(/^\s^</))
3291
- htmlStrings = htmlStrings.trim();
3313
+
3314
+ /**
3315
+ * @typedef {Object} RenderOptions
3316
+ * @property {boolean=} styles - Replace :host in style tags to scope them locally.
3317
+ * @property {boolean=} scripts - Execute script tags.
3318
+ * @property {boolean=} ids - Create references to elements with id or data-id attributes.
3319
+ * @property {?boolean} render - Deprecated.
3320
+ * Used only when options are given to a class super constructor inheriting from Solarite.
3321
+ * True to call render() immediately in super constructor.
3322
+ * False to automatically call render() at all.
3323
+ * Undefined (default) to call render() when added to the DOM, unless already rendered.
3324
+ */
3325
+
3326
+ /**
3327
+ * Convert a template, string, or object into a DOM Node or Element
3328
+ *
3329
+ * 1. h('Hello'); // Create single text node.
3330
+ * 2. h('<b>Hello</b>'); // Create single HTMLElement
3331
+ * 3. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3332
+ * 4. h(template) // Render Template created by h`<html>` or h();
3333
+ * 5. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3334
+ * @param arg {string|Template|{render:()=>void}}
3335
+ * @returns {Node|DocumentFragment|HTMLElement} */
3336
+ function toEl(arg) {
3337
+
3338
+ if (typeof arg === 'string') {
3339
+ let html = arg;
3340
+
3341
+ // If it's an element with whitespace before or after it, trim both ends.
3342
+ if (html.match(/^\s^</) || html.match(/>\s+$/))
3343
+ html = html.trim();
3292
3344
 
3293
3345
  // We create a new one each time because otherwise
3294
3346
  // the returned fragment will have its content replaced by a subsequent call.
3295
- let templateEl = document.createElement('template');
3296
- templateEl.innerHTML = htmlStrings;
3347
+ let templateEl = Globals$1.doc.createElement('template');
3348
+ templateEl.innerHTML = html;
3297
3349
 
3298
- // 4+5. Return Node if there's one child.
3350
+ // 1+2. Return Node if there's one child.
3299
3351
  let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
3300
3352
  if (relevantNodes.length === 1)
3301
3353
  return relevantNodes[0];
3302
3354
 
3303
- // 6. Otherwise return DocumentFragment.
3355
+ // 3. Otherwise return DocumentFragment.
3304
3356
  return templateEl.content;
3305
3357
  }
3306
3358
 
3307
- // 7. Create a static element
3308
- else if (htmlStrings === undefined) {
3309
- return (htmlStrings, ...exprs) => {
3310
- //Globals.rendered.add(parent)
3311
- let template = h(htmlStrings, ...exprs);
3312
- return template.render();
3313
- }
3314
- }
3315
-
3316
- // 8.
3317
- else if (htmlStrings instanceof Template) {
3318
- return htmlStrings.render();
3359
+ // 4.
3360
+ if (arg instanceof Template) {
3361
+ return arg.render();
3319
3362
  }
3320
3363
 
3321
-
3322
- // 9. Create dynamic element with render() function.
3364
+ // 5. Create dynamic element from an object with a render() function.
3323
3365
  // TODO: This path doesn't handle embeds like data-id="..."
3324
- else if (typeof htmlStrings === 'object') {
3325
- let obj = htmlStrings;
3366
+ else if (arg && typeof arg === 'object') {
3367
+ let obj = arg;
3326
3368
 
3327
- if (obj.constructor.name !== 'Object')
3369
+ if (obj.constructor.name !== 'Object')
3328
3370
  throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
3329
3371
 
3330
-
3331
- // Special rebound render path, called by normal path.
3332
- // Intercepts the main r`...` function call inside render().
3333
- if (Globals$1.objToEl.has(obj)) {
3334
- return function(...args) {
3335
- let template = h(...args);
3336
- let el = template.render();
3337
- Globals$1.objToEl.set(obj, el);
3338
- }.bind(obj);
3339
- }
3340
-
3341
3372
  // Normal path
3342
- else {
3373
+ if (!Globals$1.objToEl.has(obj)) {
3343
3374
  Globals$1.objToEl.set(obj, null);
3344
- obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
3375
+ obj[renderF](); // Calls the Special rebound render path above, when the render function calls h(this)
3345
3376
  let el = Globals$1.objToEl.get(obj);
3346
3377
  Globals$1.objToEl.delete(obj);
3347
3378
 
@@ -3349,7 +3380,7 @@ function h(htmlStrings=undefined, ...exprs) {
3349
3380
  if (typeof obj[name] === 'function')
3350
3381
  el[name] = obj[name].bind(el); // Make the "this" of functions be el.
3351
3382
  // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
3352
- // <my-element arg=${{myFunc() { return this }}}
3383
+ // <my-element arg=${{myFunc() { return this }}}
3353
3384
  else
3354
3385
  el[name] = obj[name];
3355
3386
 
@@ -3365,13 +3396,145 @@ function h(htmlStrings=undefined, ...exprs) {
3365
3396
  }
3366
3397
  }
3367
3398
 
3368
- else
3369
- throw new Error('Unsupported arguments.')
3399
+ throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
3400
+
3370
3401
  }
3371
3402
 
3403
+
3372
3404
  // Trick to prevent minifier from renaming this function.
3373
3405
  let renderF = 'render';
3374
3406
 
3407
+ /**
3408
+ * Convert strings to HTMLNodes.
3409
+ * Using h`...` as a tag will always create a Template.
3410
+ * Using h() as a function() will always create a DOM element.
3411
+ *
3412
+ * Features beyond what standard js tagged template strings do:
3413
+ * 1. r`` sub-expressions
3414
+ * 2. functions, nodes, and arrays of nodes as sub-expressions.
3415
+ * 3. html-escape all expressions by default, unless wrapped in h()
3416
+ * 4. event binding
3417
+ * 5. TODO: list more
3418
+ *
3419
+ * General rule:
3420
+ * If h() is a function with null or an HTMLElement as its first argument create a Node.
3421
+ * Otherwise create a template
3422
+ *
3423
+ * Currently supported:
3424
+ *
3425
+ * Create Tempataes
3426
+ * 1. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
3427
+ * 2. h('<b>Hello</b><u>Goodbye</u>'); // Create Template from string, that can later be used to create nodes.
3428
+ *
3429
+ * Add children to an element.
3430
+ * 3. h(el, h`<b>${'Hi'}</b>`, ?options)
3431
+ * 4. h(el, ?options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
3432
+ *
3433
+ * Create top-level element
3434
+ * 5. h()`Hello<b>${'World'}!</b>`
3435
+ *
3436
+ * 6. h(string, object, ...) // Used for JSX
3437
+ * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
3438
+ * @param exprs {*[]|string|Template|Object}
3439
+ * @return {Node|HTMLElement|Template} */
3440
+ function h(htmlStrings=undefined, ...exprs) {
3441
+
3442
+ // 1. Tagged template
3443
+ if (Array.isArray(arguments[0])) {
3444
+ return new Template(arguments[0], exprs);
3445
+ }
3446
+
3447
+ // 2. String to template, or JSX factory form h(tag, props, ...children)
3448
+ else if (typeof arguments[0] === 'string' || arguments[0] instanceof String) {
3449
+ let tagOrHtml = arguments[0];
3450
+
3451
+ // 2a. JSX: h("tag", {props}, ...children)
3452
+ if (exprs.length && (typeof exprs[0] === 'object' || exprs[0] === null)) {
3453
+ let tag = tagOrHtml + '';
3454
+ let props = exprs[0] || {};
3455
+ let children = exprs.slice(1);
3456
+
3457
+ return Template.fromJsx(tag, props, children);
3458
+ }
3459
+
3460
+ // 2b. Plain html string => template
3461
+ else {
3462
+ let html = tagOrHtml;
3463
+ // If it starts with whitespace, trim both ends.
3464
+ // TODO: Also trim if it ends with whitespace?
3465
+ if (html.match(/^\s^</))
3466
+ html = html.trim();
3467
+ return new Template([html], []);
3468
+ }
3469
+ }
3470
+
3471
+ else if (arguments[0] instanceof HTMLElement || arguments[0] instanceof DocumentFragment) {
3472
+
3473
+ // 3. Render template to element.
3474
+ if (arguments[1] instanceof Template) {
3475
+
3476
+ /** @type Template */
3477
+ let template = arguments[1];
3478
+ let parent = arguments[0];
3479
+ let options = arguments[2]; // deprecated?
3480
+ template.render(parent, options);
3481
+ }
3482
+
3483
+ // 4. Render tagged template to element
3484
+ else {
3485
+ let parent = arguments[0], options = arguments[1];
3486
+
3487
+ // Remove shadowroot. TODO: This could mess up paths?
3488
+ if (parent.shadowRoot)
3489
+ parent.innerHTML = '';
3490
+
3491
+ // Return a tagged template function that applies the tagged template to parent.
3492
+ let taggedTemplate = (htmlStrings, ...exprs) => {
3493
+ Globals$1.rendered.add(parent);
3494
+ let template = new Template(htmlStrings, exprs);
3495
+ return template.render(parent, options);
3496
+ };
3497
+ return taggedTemplate;
3498
+ }
3499
+ }
3500
+
3501
+ // 5. Create a static element h()'<div></div>' (Deprecated?)
3502
+ else if (!arguments.length) {
3503
+ return (htmlStrings, ...exprs) => {
3504
+ let template = h(htmlStrings, ...exprs);
3505
+ return toEl(template); // Go to path 6.
3506
+ }
3507
+ }
3508
+
3509
+ // 6. Help toEl() with objects.
3510
+ // Special rebound render path, called by normal path.
3511
+ // Intercepts the main h(this)`...` function call inside render().
3512
+ // TODO: This path doesn't handle embeds like data-id="..."
3513
+ else if (typeof arguments[0] === 'object' && Globals$1.objToEl.has(arguments[0])) {
3514
+ let obj = arguments[0];
3515
+
3516
+ if (obj.constructor.name !== 'Object')
3517
+ throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
3518
+
3519
+ // Jsx with h(this, <jsx>)
3520
+ if (arguments[1] instanceof Template) {
3521
+ let template = arguments[1];
3522
+ let el = template.render();
3523
+ Globals$1.objToEl.set(obj, el);
3524
+ }
3525
+
3526
+ // h(this)`<div>...</div>
3527
+ else
3528
+ return function(...args) {
3529
+ let template = h(...args);
3530
+ let el = template.render();
3531
+ Globals$1.objToEl.set(obj, el);
3532
+ }.bind(obj);
3533
+ }
3534
+ else
3535
+ throw new Error('h() does not support argument of type: ' + (arguments[0] ? typeof arguments[0] : arguments[0]))
3536
+ }
3537
+
3375
3538
  /**
3376
3539
  * There are three ways to create an instance of a Solarite Component:
3377
3540
  * 1. new ComponentName(); // direct class instantiation
@@ -3545,7 +3708,7 @@ function createSolarite(extendsTag=null) {
3545
3708
 
3546
3709
  BaseClass = Globals$1.elementClasses[extendsTag];
3547
3710
  if (!BaseClass) { // TODO: Use Cache
3548
- BaseClass = document.createElement(extendsTag).constructor;
3711
+ BaseClass = Globals$1.doc.createElement(extendsTag).constructor;
3549
3712
  Globals$1.elementClasses[extendsTag] = BaseClass;
3550
3713
  }
3551
3714
  }
@@ -3606,7 +3769,7 @@ function createSolarite(extendsTag=null) {
3606
3769
  this.innerHTML = html;
3607
3770
  }
3608
3771
  else
3609
- this.modifications = r(this, html, options);
3772
+ this.modifications = h(this, html, options);
3610
3773
  }
3611
3774
  })*/
3612
3775
 
@@ -3656,11 +3819,14 @@ function createSolarite(extendsTag=null) {
3656
3819
  let define = 'define';
3657
3820
  let getName = 'getName';
3658
3821
 
3659
- /**
3660
- * Solarite JavasCript UI library.
3661
- * MIT License
3662
- * https://vorticode.github.io/solarite/
3663
- */
3822
+ /*
3823
+ ┏┓ ┓ •
3824
+ ┗┓┏┓┃┏┓┏┓┓╋▗▖
3825
+ ┗┛┗┛┗┗┻╹ ╹╹┗
3826
+ JavasCript UI library
3827
+ @license MIT
3828
+ @copyright Vorticode LLC
3829
+ https://vorticode.github.io/solarite/ */
3664
3830
 
3665
3831
  /**
3666
3832
  * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
@@ -3674,4 +3840,4 @@ const Solarite = new Proxy(createSolarite(), {
3674
3840
  //export {default as watch, renderWatched} from './watch.js'; // unfinished
3675
3841
 
3676
3842
  export default h;
3677
- export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs };
3843
+ export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, getArg, h, h as r, setArgs, toEl };