solarite 0.2.4 → 0.3.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.
Files changed (38) hide show
  1. package/dist/Solarite-debug.js +1457 -1402
  2. package/dist/Solarite.js +1425 -1272
  3. package/dist/Solarite.min.js +2 -2
  4. package/package.json +5 -6
  5. package/readme.md +2 -4
  6. package/src/{solarite/ExprPath.js → ExprPath.js} +421 -231
  7. package/src/Globals.js +79 -0
  8. package/src/HtmlParser.js +91 -0
  9. package/src/{util/MultiValueMap.js → MultiValueMap.js} +22 -26
  10. package/src/{solarite/NodeGroup.js → NodeGroup.js} +286 -226
  11. package/src/{solarite/Shell.js → Shell.js} +119 -92
  12. package/src/Solarite.d.ts +62 -0
  13. package/src/{solarite/Solarite.js → Solarite.js} +15 -13
  14. package/src/{solarite/Template.js → Template.js} +22 -19
  15. package/src/Util.js +330 -0
  16. package/src/{util/Errors.js → assert.js} +1 -0
  17. package/src/createSolarite.js +154 -0
  18. package/src/{util/delve.js → delve.js} +5 -4
  19. package/src/{solarite/getArg.js → getArg.js} +41 -15
  20. package/src/{solarite/r.js → h.js} +59 -29
  21. package/src/{solarite/hash.js → hash.js} +12 -9
  22. package/src/unused/FastLookupArray.js +54 -0
  23. package/src/unused/Hashes.js +339 -0
  24. package/src/unused/InUse.test.js +92 -0
  25. package/src/unused/InUseMap.js +98 -0
  26. package/src/unused/LinkedList.js +117 -0
  27. package/src/unused/LinkedList.test.js +115 -0
  28. package/src/unused/Misc.js +13 -0
  29. package/src/unused/Perf.js +47 -0
  30. package/src/unused/TrackedArray.js +54 -0
  31. package/src/watch.js +546 -0
  32. package/src/solarite/Globals.js +0 -54
  33. package/src/solarite/Util.js +0 -388
  34. package/src/solarite/createSolarite.js +0 -274
  35. package/src/solarite/watch3.js +0 -189
  36. package/src/util/Util.js +0 -113
  37. /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
  38. /package/src/{util → unused}/WeakArray.js +0 -0
@@ -1,14 +1,11 @@
1
- import {assert} from "../util/Errors.js";
2
- import ExprPath, {PathType, resolveNodePath} from "./ExprPath.js";
1
+ import {assert} from "./assert.js";
2
+ import ExprPath, {ExprPathType, resolveNodePath} from "./ExprPath.js";
3
3
  import {getObjectHash} from "./hash.js";
4
4
  import Shell from "./Shell.js";
5
- import udomdiff from "./udomdiff.js";
6
- import Util, {arraySame, flattenAndIndent, isEvent, nodeToArrayTree, setIndent} from "./Util.js";
5
+ import Util, {flattenAndIndent, nodeToArrayTree, setIndent} from "./Util.js";
7
6
  //import NodeGroupManager from "./NodeGroupManager.js";
8
- import delve from "../util/delve.js";
7
+ import delve from "./delve.js";
9
8
  import Globals from "./Globals.js";
10
- import MultiValueMap from "../util/MultiValueMap.js";
11
-
12
9
 
13
10
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
14
11
 
@@ -34,7 +31,8 @@ export default class NodeGroup {
34
31
  startNode;
35
32
 
36
33
  /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
37
- * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
34
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
35
+ * TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
38
36
  endNode;
39
37
 
40
38
  /** @type {ExprPath[]} */
@@ -52,11 +50,11 @@ export default class NodeGroup {
52
50
  nodesCache;
53
51
 
54
52
  /**
53
+ * A map between <style> Elements and their text content.
54
+ * This lets NodeGroup.updateStyles() see when the style text has changed.
55
55
  * @type {?Map<HTMLStyleElement, string>} */
56
56
  styles;
57
57
 
58
- currentComponentProps = {};
59
-
60
58
 
61
59
  /**
62
60
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
@@ -64,14 +62,26 @@ export default class NodeGroup {
64
62
  * @param parentPath {?ExprPath} */
65
63
  constructor(template, parentPath=null) {
66
64
  if (!(this instanceof RootNodeGroup)) {
65
+
67
66
  let [fragment, shell] = this.init(template, parentPath);
68
67
 
69
- this.updatePaths(fragment, shell.paths);
68
+ if (fragment && template.exprs.length) {
69
+ this.updatePaths(fragment, shell.paths);
70
70
 
71
- this.activateEmbeds(fragment, shell);
71
+ // Static web components can sometimes have children created via expressions.
72
+ // But calling applyExprs() will mess up the shell's path to them.
73
+ // So we find them first, then call activateStaticComponents() after their children have been created.
74
+ let staticComponents = this.findStaticComponents(fragment, shell);
72
75
 
73
- // Apply exprs
74
- this.applyExprs(template.exprs);
76
+ this.activateEmbeds(fragment, shell);
77
+
78
+ // Apply exprs
79
+ this.applyExprs(template.exprs);
80
+
81
+ this.instantiateStaticComponents(staticComponents);
82
+ }
83
+ else if (shell)
84
+ this.activateEmbeds(fragment, shell);
75
85
  }
76
86
  }
77
87
 
@@ -99,68 +109,119 @@ export default class NodeGroup {
99
109
  template.nodeGroup = this;
100
110
 
101
111
  // Get a cached version of the parsed and instantiated html, and ExprPaths.
102
- let shell = Shell.get(template.html);
103
- let fragment = shell.fragment.cloneNode(true);
104
112
 
105
- let childNodes = fragment.childNodes;
106
- this.startNode = childNodes[0];
107
- this.endNode = childNodes[childNodes.length - 1];
113
+ // If it's just a text node, skip a bunch of unnecessary steps.
114
+ if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
115
+ //let doc = this.rootNg.startNode?.ownerDocument || document;
116
+ let textNode = document.createTextNode(template.html[0]);
108
117
 
109
- return [fragment, shell];
118
+ this.startNode = this.endNode = textNode;
119
+ return [];
120
+ }
121
+ else {
122
+ let shell = Shell.get(template.html);
123
+ let fragment = shell.fragment.cloneNode(true);
124
+
125
+ if (fragment instanceof DocumentFragment) {
126
+ let childNodes = fragment.childNodes;
127
+ this.startNode = childNodes[0];
128
+ this.endNode = childNodes[childNodes.length - 1];
129
+ }
130
+ else {
131
+ this.startNode = this.endNode = fragment;
132
+ }
133
+ return [fragment, shell];
134
+ }
110
135
  }
111
136
 
112
137
  /**
113
138
  * Use the paths to insert the given expressions.
114
139
  * Dispatches expression handling to other functions depending on the path type.
115
140
  * @param exprs {(*|*[]|function|Template)[]}
116
- * @param paths {?ExprPath[]} Optional. */
141
+ * @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
117
142
  applyExprs(exprs, paths=null) {
118
143
  paths = paths || this.paths;
119
144
 
120
- /*#IFDEV*/this.verify();/*#ENDIF*/
145
+ /*#IFDEV*/
146
+ this.verify();/*#ENDIF*/
147
+
148
+ // Things to consider:
149
+ // 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
150
+ // 2. One component may need to use multiple attribute paths to be instantiated.
151
+ // 3. We apply them in reverse order so that a <select> box has its children created from an expression
152
+ // before its instantiated and its value attribute is set via an expression.
153
+
154
+ let exprIndex = exprs.length - 1; // Update exprs at paths.
155
+ let lastComponentPathIndex;
156
+ let pathExprs = new Array(paths.length); // Store all the expressions that map to a single path. Only paths to attribute values can have more than one.
157
+ for (let i = paths.length - 1, path; path = paths[i]; i--) {
158
+ let prevPath = paths[i - 1];
159
+ let nextPath = paths[i + 1];
160
+
161
+ // Get the expressions associated with this path.
162
+ if (path.attrValue?.length > 2) {
163
+ let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
164
+ pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
165
+ exprIndex -= pathExprs[i].length;
166
+ } else {
167
+ pathExprs[i] = [exprs[exprIndex]];
168
+ exprIndex--;
169
+ }
121
170
 
122
- // Update exprs at paths.
123
- let exprIndex = exprs.length-1, expr, lastNode;
171
+ // TODO: Need to end and restart this block when going from one component to the next?
172
+ // Think of having two adjacent components.
173
+ // But the dynamicAttribsAdjacet test already passes.
124
174
 
125
- // We apply them in reverse order so that a <select> box has its options created from an expression
126
- // before its value attribute is set via an expression.
127
- for (let path of paths.toReversed()) {
128
- expr = exprs[exprIndex];
175
+ // If expr is an attribute in a component:
176
+ // 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
177
+ // 2. Otherwise send them to its render function.
178
+ // Components with no expressions as attributes are instead activated in activateEmbeds().
179
+ if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
129
180
 
130
- // Nodes
181
+ if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
182
+ lastComponentPathIndex = i;
183
+ let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
131
184
 
132
- // This is necessary both here and below.
133
- if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
134
- this.applyComponentExprs(lastNode, this.currentComponentProps);
135
- this.currentComponentProps = {};
136
- }
185
+ if (isFirstComponentPath) {
137
186
 
138
- exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
187
+ let componentProps = {}
188
+ for (let j=i; j<=lastComponentPathIndex; j++) {
189
+ let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
190
+ componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
191
+ }
139
192
 
140
- lastNode = path.nodeMarker;
193
+ this.applyComponentExprs(path.nodeMarker, componentProps);
141
194
 
195
+ // Set attributes on component.
196
+ for (let j=i; j<=lastComponentPathIndex; j++)
197
+ paths[j].apply(pathExprs[j]);
198
+ }
199
+ }
142
200
 
143
- exprIndex--;
144
- } // end for(path of this.paths)
201
+ // Else apply it normally
202
+ else
203
+ path.apply(pathExprs[i]);
145
204
 
146
205
 
147
- // Check again after we iterate through all paths to apply to a component.
148
- if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
149
- this.applyComponentExprs(lastNode, this.currentComponentProps);
150
- this.currentComponentProps = {};
151
- }
206
+ } // end for(path of this.paths)
207
+
152
208
 
209
+ // TODO: Only do this if we have ExprPaths within styles?
153
210
  this.updateStyles();
154
211
 
212
+
213
+
155
214
  // Invalidate the nodes cache because we just changed it.
156
215
  this.nodesCache = null;
157
216
 
158
217
  // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
159
218
  // and the number of paths not matching.
160
- /*#IFDEV*/assert(exprIndex === -1);/*#ENDIF*/
219
+ /*#IFDEV*/
220
+ assert(exprIndex === -1);/*#ENDIF*/
161
221
 
162
222
 
163
- /*#IFDEV*/this.verify();/*#ENDIF*/
223
+ /*#IFDEV*/
224
+ this.verify();/*#ENDIF*/
164
225
  }
165
226
 
166
227
  /**
@@ -174,24 +235,30 @@ export default class NodeGroup {
174
235
  // then we could re-use the hash and logic from NodeManager?
175
236
  let newHash = getObjectHash(props);
176
237
 
177
- let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
238
+ let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
178
239
  let isPreIsElement = el.hasAttribute('_is')
179
240
 
180
241
 
181
242
  // Instantiate a placeholder.
182
243
  if (isPreHtmlElement || isPreIsElement)
183
- el = this.createNewComponent(el, isPreHtmlElement, props);
244
+ el = this.instantiateComponent(el, isPreHtmlElement, props);
184
245
 
185
246
  // Call render() with the same params that would've been passed to the constructor.
247
+ // We do this even if the arguments haven't changed, so we can let the child component
248
+ // compare the arguments and then decide for itself whether it wants to re-render.
186
249
  else if (el.render) {
187
- let oldHash = Globals.componentHash.get(el);
188
- if (oldHash !== newHash)
189
- el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
250
+ //let oldHash = Globals.componentArgsHash.get(el);
251
+ //if (oldHash !== newHash) { // Only if not changed.
252
+ let args = {};
253
+ for (let name in props || {})
254
+ args[Util.dashesToCamel(name)] = props[name];
255
+ el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
256
+ //}
190
257
  }
191
258
 
192
- Globals.componentHash.set(el, newHash);
259
+ Globals.componentArgsHash.set(el, newHash);
193
260
  }
194
-
261
+
195
262
  /**
196
263
  * We swap the placeholder element for the real element so we can pass its dynamic attributes
197
264
  * to its constructor.
@@ -199,78 +266,54 @@ export default class NodeGroup {
199
266
  * The logic of this function is complex and could use cleaning up.
200
267
  *
201
268
  * @param el
202
- * @param isPreHtmlElement
269
+ * @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
203
270
  * @param props {Object} Attributes with dynamic values.
204
271
  * @return {HTMLElement} */
205
- createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
272
+ instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
206
273
  if (isPreHtmlElement === undefined)
207
274
  isPreHtmlElement = !el.hasAttribute('_is');
208
-
275
+
209
276
  let tagName = (isPreHtmlElement
210
- ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
211
- ? el.tagName.slice(0, -21)
212
- : el.tagName
277
+ ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
213
278
  : el.getAttribute('is')).toLowerCase();
214
279
 
215
- let dynamicProps = {...(props || {})}
216
-
280
+
281
+ // Throw if custom element isn't defined.
282
+ let Constructor = customElements.get(tagName);
283
+ if (!Constructor)
284
+ throw new Error(`The custom tag name ${tagName} is not registered.`)
285
+
286
+ let args = {};
287
+ for (let name in props || {})
288
+ args[Util.dashesToCamel(name)] = props[name];
289
+
217
290
  // Pass other attribs to constructor, since otherwise they're not yet set on the element,
218
291
  // and the constructor would otherwise have no way to see them.
219
292
  if (el.attributes.length) {
220
- if (!props)
221
- props = {};
222
- for (let attrib of el.attributes)
223
- if (!props.hasOwnProperty(attrib.name))
224
- props[attrib.name] = attrib.value;
293
+ for (let attrib of el.attributes) {
294
+ let attribName = Util.dashesToCamel(attrib.name);
295
+ if (!args.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
296
+ args[attribName] = attrib.value;
297
+ }
225
298
  }
226
-
227
- // Create CustomElement and
228
- let Constructor = customElements.get(tagName);
229
- if (!Constructor)
230
- throw new Error(`The custom tag name ${tagName} is not registered.`)
231
299
 
232
- // We pass the childNodes to the constructor so it can know about them,
233
- // instead of only afterward when they're appended to the slot below.
234
- // This is useful for a custom selectbox, for example.
235
- // Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
236
- // can add them as children before the rest of the constructor code executes.
237
- let ch = [... el.childNodes];
238
- Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
239
- let newEl = new Constructor(props, ch);
300
+ // Create the web component.
301
+ // Get the children that aren't Solarite's comment placeholders.
302
+ let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
303
+ let newEl = new Constructor(args, ch);
240
304
 
241
305
  if (!isPreHtmlElement)
242
- newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
306
+ newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
307
+
308
+ // Replace the placeholder tag with the instantiated web component.
243
309
  el.replaceWith(newEl);
244
310
 
245
- // Set children / slot children
246
- // TODO: Match named slots.
247
- // TODO: This only appends to slot if render() is called in the constructor.
248
- //let slot = newEl.querySelector('slot') || newEl;
249
- //slot.append(...el.childNodes);
250
-
251
- // Copy over event attributes.
252
- for (let propName in props) {
253
- let val = props[propName];
254
- if (propName.startsWith('on') && typeof val === 'function')
255
- newEl.addEventListener(propName.slice(2), e => val(e, newEl));
256
-
257
- // Bind array based event attributes on value.
258
- // This same logic is in ExprPath.applyValueAttrib() for non-components.
259
- if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
260
- let [obj, path] = [val[0], val.slice(1)];
261
- newEl.value = delve(obj, path);
262
- newEl.addEventListener('input', e => {
263
- delve(obj, path, Util.getInputValue(newEl));
264
- }, true); // We use capture so we update the values before other events added by the user.
265
- }
266
- }
267
-
268
311
  // If an id pointed at the placeholder, update it to point to the new element.
269
312
  let id = el.getAttribute('data-id') || el.getAttribute('id');
270
313
  if (id)
271
314
  delve(this.getRootNode(), id.split(/\./g), newEl);
272
-
273
-
315
+
316
+
274
317
  // Update paths to use replaced element.
275
318
  for (let path of this.paths) {
276
319
  if (path.nodeMarker === el)
@@ -282,31 +325,31 @@ export default class NodeGroup {
282
325
  this.startNode = newEl;
283
326
  if (this.endNode === el)
284
327
  this.endNode = newEl;
285
-
286
-
328
+
329
+
287
330
  // applyComponentExprs() is called because we're rendering.
288
331
  // So we want to render the sub-component also.
289
332
  if (newEl.renderFirstTime)
290
333
  newEl.renderFirstTime();
291
-
334
+
292
335
  // Copy attributes over.
293
336
  for (let attrib of el.attributes)
294
- if (attrib.name !== '_is')
337
+ if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
295
338
  newEl.setAttribute(attrib.name, attrib.value);
296
339
 
297
340
  // Set dynamic attributes if they are primitive types.
298
- for (let name in dynamicProps) {
299
- let val = dynamicProps[name];
341
+ for (let name in props) {
342
+ let val = props[name];
300
343
  if (typeof val === 'boolean') {
301
344
  if (val !== false && val !== undefined && val !== null)
302
345
  newEl.setAttribute(name, '');
303
346
  }
304
347
 
305
- // If type isn't an object or array, set the attribute.
348
+ // If type is a non-boolean primitive, set the attribute value.
306
349
  else if (['number', 'bigint', 'string'].includes(typeof val))
307
350
  newEl.setAttribute(name, val);
308
351
  }
309
-
352
+
310
353
  return newEl;
311
354
  }
312
355
 
@@ -352,8 +395,7 @@ export default class NodeGroup {
352
395
 
353
396
  /**
354
397
  * Requires the nodeCache to be present. */
355
- saveOrphans() {
356
- /*#IFDEV*/assert(!this.startNode.parentNode);/*#ENDIF*/
398
+ removeAndSaveOrphans() {
357
399
  /*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
358
400
  let fragment = document.createDocumentFragment();
359
401
  for (let node of this.getNodes())
@@ -363,8 +405,9 @@ export default class NodeGroup {
363
405
 
364
406
  updatePaths(fragment, paths, offset) {
365
407
  // Update paths to point to the fragment.
366
- this.paths.length = paths.length;
367
- for (let i=0; i<paths.length; i++) {
408
+ let pathLength = paths.length;
409
+ this.paths.length = pathLength;
410
+ for (let i=0; i<pathLength; i++) {
368
411
  let path = paths[i].clone(fragment, offset)
369
412
  path.parentNg = this;
370
413
  this.paths[i] = path;
@@ -386,23 +429,23 @@ export default class NodeGroup {
386
429
  * An interleaved array of sets of nodes and top-level ExprPaths
387
430
  * @type {(Node|HTMLElement|ExprPath)[]} */
388
431
  get nodes() { throw new Error('')};
389
-
432
+
390
433
  get debug() {
391
434
  return [
392
435
  `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
393
436
  'nodes:',
394
437
  ...setIndent(this.getNodes().map(item => {
395
438
  if (item instanceof Node) {
396
-
439
+
397
440
  let tree = nodeToArrayTree(item, nextNode => {
398
-
399
- let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
441
+
442
+ let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
400
443
  if (path)
401
444
  return [`Path.nodes:`]
402
-
445
+
403
446
  return [];
404
447
  })
405
-
448
+
406
449
  // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
407
450
  return flattenAndIndent(tree)
408
451
  }
@@ -413,10 +456,10 @@ export default class NodeGroup {
413
456
  }
414
457
 
415
458
  get debugNodes() { return this.getNodes() }
416
-
417
-
459
+
460
+
418
461
  get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
419
-
462
+
420
463
  verify() {
421
464
  if (!window.verify)
422
465
  return;
@@ -431,7 +474,7 @@ export default class NodeGroup {
431
474
 
432
475
  // if (this.parentPath)
433
476
  // assert(this.parentPath.nodeGroups.includes(this));
434
-
477
+
435
478
  for (let path of this.paths) {
436
479
  assert(path.parentNg === this)
437
480
 
@@ -447,61 +490,70 @@ export default class NodeGroup {
447
490
  }
448
491
  //#ENDIF
449
492
 
493
+ findStaticComponents(root, shell, pathOffset=0) {
494
+ let result = [];
450
495
 
451
- /**
452
- * @param root {HTMLElement}
453
- * @param shell {Shell}
454
- * @param pathOffset {int} */
455
- activateEmbeds(root, shell, pathOffset=0) {
456
-
457
- // static components. These are WebComponents not created by an expression.
458
- // Must happen before ids.
496
+ // static components. These are WebComponents that do not have any constructor arguments that are expressions.
497
+ // Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
498
+ // Maybe someday these two paths will be merged?
499
+ // Must happen before ids because instantiateComponent will replace the element.
459
500
  for (let path of shell.staticComponents) {
460
501
  if (pathOffset)
461
502
  path = path.slice(0, -pathOffset);
462
503
  let el = resolveNodePath(root, path);
463
504
 
464
505
  // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
506
+ // Recreating it is necessary so we can pass the constructor args to it.
465
507
  if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
466
- this.createNewComponent(el)
508
+ result.push(el);
467
509
  }
510
+ return result;
511
+ }
512
+
513
+ instantiateStaticComponents(staticComponents) {
514
+ for (let el of staticComponents)
515
+ this.instantiateComponent(el)
516
+ }
517
+
518
+ /**
519
+ * @param root {HTMLElement|DocumentFragment}
520
+ * @param shell {Shell}
521
+ * @param pathOffset {int} */
522
+ activateEmbeds(root, shell, pathOffset=0) {
468
523
 
469
524
  let rootEl = this.rootNg.root;
470
525
  if (rootEl) {
526
+ let options = this.rootNg.options;
471
527
 
472
528
  // ids
473
- if (this.options?.ids !== false)
529
+ if (options?.ids !== false) {
474
530
  for (let path of shell.ids) {
475
531
  if (pathOffset)
476
532
  path = path.slice(0, -pathOffset);
477
533
  let el = resolveNodePath(root, path);
478
- let id = el.getAttribute('data-id') || el.getAttribute('id');
479
- if (id) { // If something hasn't removed the id.
480
-
481
- // Don't allow overwriting existing class properties if they already have a non-Node value.
482
- if (rootEl[id] && !(rootEl[id] instanceof Node))
483
- throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
484
- `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
485
-
486
- delve(rootEl, id.split(/\./g), el);
487
- }
534
+ Util.bindId(rootEl, el);
535
+ }
488
536
  }
489
537
 
490
538
  // styles
491
- if (this.options?.styles !== false) {
539
+ if (options?.styles !== false) {
492
540
  if (shell.styles.length)
493
541
  this.styles = new Map();
494
542
  for (let path of shell.styles) {
495
543
  if (pathOffset)
496
544
  path = path.slice(0, -pathOffset);
545
+
546
+ /** @type {HTMLStyleElement} */
497
547
  let style = resolveNodePath(root, path);
498
- Util.bindStyles(style, rootEl);
499
- this.styles.set(style, style.textContent);
548
+ if (rootEl.nodeType === 1) {
549
+ Util.bindStyles(style, rootEl);
550
+ this.styles.set(style, style.textContent);
551
+ }
500
552
  }
501
553
 
502
554
  }
503
555
  // scripts
504
- if (this.options?.scripts !== false) {
556
+ if (options?.scripts !== false) {
505
557
  for (let path of shell.scripts) {
506
558
  if (pathOffset)
507
559
  path = path.slice(0, -pathOffset);
@@ -522,20 +574,8 @@ export class RootNodeGroup extends NodeGroup {
522
574
  root;
523
575
 
524
576
  /**
525
- * Store the expressions that use this watched variable,
526
- * along with the functions used to get their values.
527
- * @type {Object<field:string, Set<ExprPath>>} */
528
- watchedExprPaths = {};
529
-
530
- /**
531
- * Map from arrays where .map is called and their callback functions.
532
- * TODO: One array might be called with two different map functions in different places!
533
- * @type {Map<Array, function>} */
534
- mapCallbacks = new Map();
535
-
536
- /**
537
- *
538
- * @type {Map<ExprPath, boolean|Array>} */
577
+ * When we call renerWatched() we re-render these expressions, then clear this to a new Map()
578
+ * @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
539
579
  exprsToRender = new Map();
540
580
 
541
581
  /**
@@ -552,82 +592,102 @@ export class RootNodeGroup extends NodeGroup {
552
592
  this.rootNg = this;
553
593
  let [fragment, shell] = this.init(template);
554
594
 
555
- // If adding NodeGroup to an element.
556
- let offset = 0;
557
- let root = fragment; // TODO: Rename so it's not confused with this.root.
558
- if (el) {
559
- Globals.nodeGroups.set(el, this);
560
-
561
- // Save slot children
562
- let slotFragment;
563
- if (el.childNodes.length) {
564
- slotFragment = document.createDocumentFragment();
565
- slotFragment.append(...el.childNodes);
595
+ if (fragment instanceof Text) {
596
+
597
+ if (el) {
598
+ this.startNode = el;
599
+ this.endNode = el;
600
+ if (fragment.nodeValue.length)
601
+ el.append(fragment);
602
+ this.root = el;
566
603
  }
604
+ Globals.nodeGroups.set(this.root, this);
605
+ }
606
+ else {
567
607
 
568
- this.root = el;
608
+ // If adding NodeGroup to an element.
609
+ let offset = 0;
610
+ let root = fragment; // TODO: Rename so it's not confused with this.root.
611
+ if (el) {
612
+ Globals.nodeGroups.set(el, this);
613
+
614
+ // Save slot children
615
+ let slotChildren;
616
+ if (el.childNodes.length) {
617
+ slotChildren = document.createDocumentFragment();
618
+ slotChildren.append(...el.childNodes);
619
+ }
569
620
 
570
- // If el should replace the root node of the fragment.
571
- if (isReplaceEl(fragment, el)) {
572
- el.append(...fragment.children[0].childNodes);
621
+ this.root = el;
573
622
 
574
- // Copy attributes
575
- for (let attrib of fragment.children[0].attributes)
576
- if (!el.hasAttribute(attrib.name))
577
- el.setAttribute(attrib.name, attrib.value);
623
+ // If el should replace the root node of the fragment.
624
+ if (isReplaceEl(fragment, el)) {
625
+ el.append(...fragment.children[0].childNodes);
578
626
 
579
- // Go one level deeper into all of shell's paths.
580
- offset = 1;
581
- }
582
- else {
583
- let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
584
- if (!isEmpty)
585
- el.append(...fragment.childNodes);
586
- }
627
+ // Copy attributes
628
+ for (let attrib of fragment.children[0].attributes)
629
+ if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
630
+ el.setAttribute(attrib.name, attrib.value);
631
+
632
+ // Go one level deeper into all of shell's paths.
633
+ offset = 1;
634
+ } else {
635
+ let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
636
+ if (!isEmpty)
637
+ el.append(...fragment.childNodes);
638
+ }
639
+
640
+ // Setup children
641
+ if (slotChildren) {
587
642
 
588
- // Setup slots
589
- if (slotFragment) {
590
- for (let slot of el.querySelectorAll('slot[name]')) {
591
- let name = slot.getAttribute('name')
592
- if (name) {
593
- let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
594
- slot.append(...slotChildren);
643
+ // Named slots
644
+ for (let slot of el.querySelectorAll('slot[name]')) {
645
+ let name = slot.getAttribute('name')
646
+ if (name) {
647
+ let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
648
+ slot.append(...slotChildren2);
649
+ }
595
650
  }
651
+
652
+ // Unnamed slots
653
+ let unamedSlot = el.querySelector('slot:not([name])')
654
+ if (unamedSlot)
655
+ unamedSlot.append(slotChildren);
656
+
657
+ // No slots
658
+ else
659
+ el.append(slotChildren);
596
660
  }
597
- let unamedSlot = el.querySelector('slot:not([name])')
598
- if (unamedSlot)
599
- unamedSlot.append(slotFragment);
600
- else
601
- el.append(slotFragment);
602
- }
603
661
 
604
- root = el;
662
+ root = el;
605
663
 
606
- this.startNode = el;
607
- this.endNode = el;
608
- }
609
- else {
610
- let singleEl = getSingleEl(fragment);
611
- this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
664
+ this.startNode = el;
665
+ this.endNode = el;
666
+ } else {
667
+ let singleEl = getSingleEl(fragment);
668
+ this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
612
669
 
613
- Globals.nodeGroups.set(this.root, this);
614
- if (singleEl) {
615
- root = singleEl;
616
- offset = 1;
670
+ Globals.nodeGroups.set(this.root, this);
671
+ if (singleEl) {
672
+ root = singleEl;
673
+ offset = 1;
674
+ }
617
675
  }
618
- }
619
676
 
620
- this.updatePaths(root, shell.paths, offset);
677
+ this.updatePaths(root, shell.paths, offset);
621
678
 
622
- this.activateEmbeds(root, shell, offset);
679
+ // Static web components can sometimes have children created via expressions.
680
+ // But calling applyExprs() will mess up the shell's path to them.
681
+ // So we find them first, then call activateStaticComponents() after their children have been created.
682
+ let staticComponents = this.findStaticComponents(root, shell, offset);
623
683
 
624
- // Apply exprs
625
- this.applyExprs(template.exprs);
626
- }
684
+ this.activateEmbeds(root, shell, offset);
685
+
686
+ // Apply exprs
687
+ this.applyExprs(template.exprs);
627
688
 
628
- clearRenderWatched() {
629
- this.watchedExprPaths = {};
630
- this.mapCallbacks = new Map();
689
+ this.instantiateStaticComponents(staticComponents);
690
+ }
631
691
  }
632
692
  }
633
693
 
@@ -649,7 +709,7 @@ function getSingleEl(fragment) {
649
709
  * @param el {HTMLElement}
650
710
  * @returns {boolean} */
651
711
  function isReplaceEl(fragment, el) {
652
- return el.tagName.includes('-')
653
- && fragment.children.length===1
712
+ return fragment.children.length===1
713
+ && el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
654
714
  && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
655
715
  }