solarite 0.2.4 → 0.3.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.
@@ -1,14 +1,11 @@
1
1
  import {assert} from "../util/Errors.js";
2
- import ExprPath, {PathType, resolveNodePath} from "./ExprPath.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
7
  import delve from "../util/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.activateStaticComponents(staticComponents);
82
+ }
83
+ else if (shell)
84
+ this.activateEmbeds(fragment, shell);
75
85
  }
76
86
  }
77
87
 
@@ -99,57 +109,104 @@ 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 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
 
155
212
  // Invalidate the nodes cache because we just changed it.
@@ -157,10 +214,12 @@ export default class NodeGroup {
157
214
 
158
215
  // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
159
216
  // and the number of paths not matching.
160
- /*#IFDEV*/assert(exprIndex === -1);/*#ENDIF*/
217
+ /*#IFDEV*/
218
+ assert(exprIndex === -1);/*#ENDIF*/
161
219
 
162
220
 
163
- /*#IFDEV*/this.verify();/*#ENDIF*/
221
+ /*#IFDEV*/
222
+ this.verify();/*#ENDIF*/
164
223
  }
165
224
 
166
225
  /**
@@ -184,14 +243,18 @@ export default class NodeGroup {
184
243
 
185
244
  // Call render() with the same params that would've been passed to the constructor.
186
245
  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.
246
+ let oldHash = Globals.componentArgsHash.get(el);
247
+ if (oldHash !== newHash) {
248
+ let args = {};
249
+ for (let name in props || {})
250
+ args[Util.dashesToCamel(name)] = props[name];
251
+ el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
252
+ }
190
253
  }
191
254
 
192
- Globals.componentHash.set(el, newHash);
255
+ Globals.componentArgsHash.set(el, newHash);
193
256
  }
194
-
257
+
195
258
  /**
196
259
  * We swap the placeholder element for the real element so we can pass its dynamic attributes
197
260
  * to its constructor.
@@ -205,72 +268,48 @@ export default class NodeGroup {
205
268
  createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
206
269
  if (isPreHtmlElement === undefined)
207
270
  isPreHtmlElement = !el.hasAttribute('_is');
208
-
271
+
209
272
  let tagName = (isPreHtmlElement
210
- ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
211
- ? el.tagName.slice(0, -21)
212
- : el.tagName
273
+ ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
213
274
  : el.getAttribute('is')).toLowerCase();
214
275
 
215
- let dynamicProps = {...(props || {})}
216
-
276
+
277
+ // Throw if custom element isn't defined.
278
+ let Constructor = customElements.get(tagName);
279
+ if (!Constructor)
280
+ throw new Error(`The custom tag name ${tagName} is not registered.`)
281
+
282
+ let args = {};
283
+ for (let name in props || {})
284
+ args[Util.dashesToCamel(name)] = props[name];
285
+
217
286
  // Pass other attribs to constructor, since otherwise they're not yet set on the element,
218
287
  // and the constructor would otherwise have no way to see them.
219
288
  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;
289
+ for (let attrib of el.attributes) {
290
+ let attribName = Util.dashesToCamel(attrib.name);
291
+ if (!args.hasOwnProperty(attribName))
292
+ args[attribName] = attrib.value;
293
+ }
225
294
  }
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
295
 
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);
296
+ // Create the web component.
297
+ // Get the children that aren't Solarite's comment placeholders.
298
+ let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
299
+ let newEl = new Constructor(args, ch);
240
300
 
241
301
  if (!isPreHtmlElement)
242
- newEl.setAttribute('is', el.getAttribute('is').toLowerCase())
302
+ newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
303
+
304
+ // Replace the placeholder tag with the instantiated web component.
243
305
  el.replaceWith(newEl);
244
306
 
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
307
  // If an id pointed at the placeholder, update it to point to the new element.
269
308
  let id = el.getAttribute('data-id') || el.getAttribute('id');
270
309
  if (id)
271
310
  delve(this.getRootNode(), id.split(/\./g), newEl);
272
-
273
-
311
+
312
+
274
313
  // Update paths to use replaced element.
275
314
  for (let path of this.paths) {
276
315
  if (path.nodeMarker === el)
@@ -282,31 +321,31 @@ export default class NodeGroup {
282
321
  this.startNode = newEl;
283
322
  if (this.endNode === el)
284
323
  this.endNode = newEl;
285
-
286
-
324
+
325
+
287
326
  // applyComponentExprs() is called because we're rendering.
288
327
  // So we want to render the sub-component also.
289
328
  if (newEl.renderFirstTime)
290
329
  newEl.renderFirstTime();
291
-
330
+
292
331
  // Copy attributes over.
293
332
  for (let attrib of el.attributes)
294
333
  if (attrib.name !== '_is')
295
334
  newEl.setAttribute(attrib.name, attrib.value);
296
335
 
297
336
  // Set dynamic attributes if they are primitive types.
298
- for (let name in dynamicProps) {
299
- let val = dynamicProps[name];
337
+ for (let name in props) {
338
+ let val = props[name];
300
339
  if (typeof val === 'boolean') {
301
340
  if (val !== false && val !== undefined && val !== null)
302
341
  newEl.setAttribute(name, '');
303
342
  }
304
343
 
305
- // If type isn't an object or array, set the attribute.
344
+ // If type is a non-boolean primitive, set the attribute value.
306
345
  else if (['number', 'bigint', 'string'].includes(typeof val))
307
346
  newEl.setAttribute(name, val);
308
347
  }
309
-
348
+
310
349
  return newEl;
311
350
  }
312
351
 
@@ -352,8 +391,7 @@ export default class NodeGroup {
352
391
 
353
392
  /**
354
393
  * Requires the nodeCache to be present. */
355
- saveOrphans() {
356
- /*#IFDEV*/assert(!this.startNode.parentNode);/*#ENDIF*/
394
+ removeAndSaveOrphans() {
357
395
  /*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
358
396
  let fragment = document.createDocumentFragment();
359
397
  for (let node of this.getNodes())
@@ -363,8 +401,9 @@ export default class NodeGroup {
363
401
 
364
402
  updatePaths(fragment, paths, offset) {
365
403
  // Update paths to point to the fragment.
366
- this.paths.length = paths.length;
367
- for (let i=0; i<paths.length; i++) {
404
+ let pathLength = paths.length;
405
+ this.paths.length = pathLength;
406
+ for (let i=0; i<pathLength; i++) {
368
407
  let path = paths[i].clone(fragment, offset)
369
408
  path.parentNg = this;
370
409
  this.paths[i] = path;
@@ -386,23 +425,23 @@ export default class NodeGroup {
386
425
  * An interleaved array of sets of nodes and top-level ExprPaths
387
426
  * @type {(Node|HTMLElement|ExprPath)[]} */
388
427
  get nodes() { throw new Error('')};
389
-
428
+
390
429
  get debug() {
391
430
  return [
392
431
  `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
393
432
  'nodes:',
394
433
  ...setIndent(this.getNodes().map(item => {
395
434
  if (item instanceof Node) {
396
-
435
+
397
436
  let tree = nodeToArrayTree(item, nextNode => {
398
-
399
- let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
437
+
438
+ let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
400
439
  if (path)
401
440
  return [`Path.nodes:`]
402
-
441
+
403
442
  return [];
404
443
  })
405
-
444
+
406
445
  // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
407
446
  return flattenAndIndent(tree)
408
447
  }
@@ -413,10 +452,10 @@ export default class NodeGroup {
413
452
  }
414
453
 
415
454
  get debugNodes() { return this.getNodes() }
416
-
417
-
455
+
456
+
418
457
  get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
419
-
458
+
420
459
  verify() {
421
460
  if (!window.verify)
422
461
  return;
@@ -431,7 +470,7 @@ export default class NodeGroup {
431
470
 
432
471
  // if (this.parentPath)
433
472
  // assert(this.parentPath.nodeGroups.includes(this));
434
-
473
+
435
474
  for (let path of this.paths) {
436
475
  assert(path.parentNg === this)
437
476
 
@@ -447,61 +486,70 @@ export default class NodeGroup {
447
486
  }
448
487
  //#ENDIF
449
488
 
489
+ findStaticComponents(root, shell, pathOffset=0) {
490
+ let result = [];
450
491
 
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.
492
+ // static components. These are WebComponents that do not have any constructor arguments that are expressions.
493
+ // Those are instead created by applyExpr() which calls applyComponentExprs() which calls createNewcomponent().
494
+ // Maybe someday these two paths will be merged?
495
+ // Must happen before ids because createNewComponent will replace the element.
459
496
  for (let path of shell.staticComponents) {
460
497
  if (pathOffset)
461
498
  path = path.slice(0, -pathOffset);
462
499
  let el = resolveNodePath(root, path);
463
500
 
464
501
  // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
502
+ // Recreating it is necessary so we can pass the constructor args to it.
465
503
  if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
466
- this.createNewComponent(el)
504
+ result.push(el);
467
505
  }
506
+ return result;
507
+ }
508
+
509
+ activateStaticComponents(staticComponents) {
510
+ for (let el of staticComponents)
511
+ this.createNewComponent(el)
512
+ }
513
+
514
+ /**
515
+ * @param root {HTMLElement}
516
+ * @param shell {Shell}
517
+ * @param pathOffset {int} */
518
+ activateEmbeds(root, shell, pathOffset=0) {
468
519
 
469
520
  let rootEl = this.rootNg.root;
470
521
  if (rootEl) {
522
+ let options = this.rootNg.options;
471
523
 
472
524
  // ids
473
- if (this.options?.ids !== false)
525
+ if (options?.ids !== false) {
474
526
  for (let path of shell.ids) {
475
527
  if (pathOffset)
476
528
  path = path.slice(0, -pathOffset);
477
529
  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
- }
530
+ Util.bindId(rootEl, el);
531
+ }
488
532
  }
489
533
 
490
534
  // styles
491
- if (this.options?.styles !== false) {
535
+ if (options?.styles !== false) {
492
536
  if (shell.styles.length)
493
537
  this.styles = new Map();
494
538
  for (let path of shell.styles) {
495
539
  if (pathOffset)
496
540
  path = path.slice(0, -pathOffset);
541
+
542
+ /** @type {HTMLStyleElement} */
497
543
  let style = resolveNodePath(root, path);
498
- Util.bindStyles(style, rootEl);
499
- this.styles.set(style, style.textContent);
544
+ if (rootEl.nodeType === 1) {
545
+ Util.bindStyles(style, rootEl);
546
+ this.styles.set(style, style.textContent);
547
+ }
500
548
  }
501
549
 
502
550
  }
503
551
  // scripts
504
- if (this.options?.scripts !== false) {
552
+ if (options?.scripts !== false) {
505
553
  for (let path of shell.scripts) {
506
554
  if (pathOffset)
507
555
  path = path.slice(0, -pathOffset);
@@ -522,20 +570,8 @@ export class RootNodeGroup extends NodeGroup {
522
570
  root;
523
571
 
524
572
  /**
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>} */
573
+ * When we call renerWatched() we re-render these expressions, then clear this to a new Map()
574
+ * @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
539
575
  exprsToRender = new Map();
540
576
 
541
577
  /**
@@ -552,82 +588,102 @@ export class RootNodeGroup extends NodeGroup {
552
588
  this.rootNg = this;
553
589
  let [fragment, shell] = this.init(template);
554
590
 
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);
591
+ if (fragment instanceof Text) {
592
+
593
+ if (el) {
594
+ this.startNode = el;
595
+ this.endNode = el;
596
+ if (fragment.nodeValue.length)
597
+ el.append(fragment);
598
+ this.root = el;
566
599
  }
600
+ Globals.nodeGroups.set(this.root, this);
601
+ }
602
+ else {
567
603
 
568
- this.root = el;
604
+ // If adding NodeGroup to an element.
605
+ let offset = 0;
606
+ let root = fragment; // TODO: Rename so it's not confused with this.root.
607
+ if (el) {
608
+ Globals.nodeGroups.set(el, this);
609
+
610
+ // Save slot children
611
+ let slotChildren;
612
+ if (el.childNodes.length) {
613
+ slotChildren = document.createDocumentFragment();
614
+ slotChildren.append(...el.childNodes);
615
+ }
569
616
 
570
- // If el should replace the root node of the fragment.
571
- if (isReplaceEl(fragment, el)) {
572
- el.append(...fragment.children[0].childNodes);
617
+ this.root = el;
573
618
 
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);
619
+ // If el should replace the root node of the fragment.
620
+ if (isReplaceEl(fragment, el)) {
621
+ el.append(...fragment.children[0].childNodes);
578
622
 
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
- }
623
+ // Copy attributes
624
+ for (let attrib of fragment.children[0].attributes)
625
+ if (!el.hasAttribute(attrib.name))
626
+ el.setAttribute(attrib.name, attrib.value);
627
+
628
+ // Go one level deeper into all of shell's paths.
629
+ offset = 1;
630
+ } else {
631
+ let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
632
+ if (!isEmpty)
633
+ el.append(...fragment.childNodes);
634
+ }
635
+
636
+ // Setup children
637
+ if (slotChildren) {
587
638
 
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);
639
+ // Named slots
640
+ for (let slot of el.querySelectorAll('slot[name]')) {
641
+ let name = slot.getAttribute('name')
642
+ if (name) {
643
+ let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
644
+ slot.append(...slotChildren2);
645
+ }
595
646
  }
647
+
648
+ // Unnamed slots
649
+ let unamedSlot = el.querySelector('slot:not([name])')
650
+ if (unamedSlot)
651
+ unamedSlot.append(slotChildren);
652
+
653
+ // No slots
654
+ else
655
+ el.append(slotChildren);
596
656
  }
597
- let unamedSlot = el.querySelector('slot:not([name])')
598
- if (unamedSlot)
599
- unamedSlot.append(slotFragment);
600
- else
601
- el.append(slotFragment);
602
- }
603
657
 
604
- root = el;
658
+ root = el;
605
659
 
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.
660
+ this.startNode = el;
661
+ this.endNode = el;
662
+ } else {
663
+ let singleEl = getSingleEl(fragment);
664
+ this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
612
665
 
613
- Globals.nodeGroups.set(this.root, this);
614
- if (singleEl) {
615
- root = singleEl;
616
- offset = 1;
666
+ Globals.nodeGroups.set(this.root, this);
667
+ if (singleEl) {
668
+ root = singleEl;
669
+ offset = 1;
670
+ }
617
671
  }
618
- }
619
672
 
620
- this.updatePaths(root, shell.paths, offset);
673
+ this.updatePaths(root, shell.paths, offset);
621
674
 
622
- this.activateEmbeds(root, shell, offset);
675
+ // Static web components can sometimes have children created via expressions.
676
+ // But calling applyExprs() will mess up the shell's path to them.
677
+ // So we find them first, then call activateStaticComponents() after their children have been created.
678
+ let staticComponents = this.findStaticComponents(root, shell, offset);
623
679
 
624
- // Apply exprs
625
- this.applyExprs(template.exprs);
626
- }
680
+ this.activateEmbeds(root, shell, offset);
681
+
682
+ // Apply exprs
683
+ this.applyExprs(template.exprs);
627
684
 
628
- clearRenderWatched() {
629
- this.watchedExprPaths = {};
630
- this.mapCallbacks = new Map();
685
+ this.activateStaticComponents(staticComponents);
686
+ }
631
687
  }
632
688
  }
633
689
 
@@ -649,7 +705,7 @@ function getSingleEl(fragment) {
649
705
  * @param el {HTMLElement}
650
706
  * @returns {boolean} */
651
707
  function isReplaceEl(fragment, el) {
652
- return el.tagName.includes('-')
653
- && fragment.children.length===1
708
+ return fragment.children.length===1
709
+ && el.tagName.includes('-')
654
710
  && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
655
711
  }