solarite 0.2.3 → 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,12 @@
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
9
 
11
-
12
10
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
13
11
 
14
12
  /**
@@ -33,7 +31,8 @@ export default class NodeGroup {
33
31
  startNode;
34
32
 
35
33
  /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
36
- * 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. */
37
36
  endNode;
38
37
 
39
38
  /** @type {ExprPath[]} */
@@ -51,11 +50,11 @@ export default class NodeGroup {
51
50
  nodesCache;
52
51
 
53
52
  /**
53
+ * A map between <style> Elements and their text content.
54
+ * This lets NodeGroup.updateStyles() see when the style text has changed.
54
55
  * @type {?Map<HTMLStyleElement, string>} */
55
56
  styles;
56
57
 
57
- currentComponentProps = {};
58
-
59
58
 
60
59
  /**
61
60
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
@@ -63,14 +62,26 @@ export default class NodeGroup {
63
62
  * @param parentPath {?ExprPath} */
64
63
  constructor(template, parentPath=null) {
65
64
  if (!(this instanceof RootNodeGroup)) {
65
+
66
66
  let [fragment, shell] = this.init(template, parentPath);
67
67
 
68
- this.updatePaths(fragment, shell.paths);
68
+ if (fragment && template.exprs.length) {
69
+ this.updatePaths(fragment, shell.paths);
69
70
 
70
- 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);
71
75
 
72
- // Apply exprs
73
- 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);
74
85
  }
75
86
  }
76
87
 
@@ -98,57 +109,104 @@ export default class NodeGroup {
98
109
  template.nodeGroup = this;
99
110
 
100
111
  // Get a cached version of the parsed and instantiated html, and ExprPaths.
101
- let shell = Shell.get(template.html);
102
- let fragment = shell.fragment.cloneNode(true);
103
112
 
104
- let childNodes = fragment.childNodes;
105
- this.startNode = childNodes[0];
106
- 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]);
107
117
 
108
- 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
+ }
109
135
  }
110
136
 
111
137
  /**
112
138
  * Use the paths to insert the given expressions.
113
139
  * Dispatches expression handling to other functions depending on the path type.
114
140
  * @param exprs {(*|*[]|function|Template)[]}
115
- * @param paths {?ExprPath[]} Optional. */
141
+ * @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
116
142
  applyExprs(exprs, paths=null) {
117
143
  paths = paths || this.paths;
118
144
 
119
- /*#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
+ }
120
170
 
121
- // Update exprs at paths.
122
- 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.
123
174
 
124
- // We apply them in reverse order so that a <select> box has its options created from an expression
125
- // before its value attribute is set via an expression.
126
- for (let path of paths.toReversed()) {
127
- 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()) {
128
180
 
129
- // Nodes
181
+ if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
182
+ lastComponentPathIndex = i;
183
+ let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
130
184
 
131
- // This is necessary both here and below.
132
- if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
133
- this.applyComponentExprs(lastNode, this.currentComponentProps);
134
- this.currentComponentProps = {};
135
- }
185
+ if (isFirstComponentPath) {
136
186
 
137
- 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
+ }
138
192
 
139
- lastNode = path.nodeMarker;
193
+ this.applyComponentExprs(path.nodeMarker, componentProps);
140
194
 
195
+ // Set attributes on component.
196
+ for (let j=i; j<=lastComponentPathIndex; j++)
197
+ paths[j].apply(pathExprs[j]);
198
+ }
199
+ }
141
200
 
142
- exprIndex--;
143
- } // end for(path of this.paths)
201
+ // Else apply it normally
202
+ else
203
+ path.apply(pathExprs[i]);
144
204
 
145
205
 
146
- // Check again after we iterate through all paths to apply to a component.
147
- if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
148
- this.applyComponentExprs(lastNode, this.currentComponentProps);
149
- this.currentComponentProps = {};
150
- }
206
+ } // end for(path of this.paths)
207
+
151
208
 
209
+ // TODO: Only do this if we have ExprPaths within styles?
152
210
  this.updateStyles();
153
211
 
154
212
  // Invalidate the nodes cache because we just changed it.
@@ -156,10 +214,12 @@ export default class NodeGroup {
156
214
 
157
215
  // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
158
216
  // and the number of paths not matching.
159
- /*#IFDEV*/assert(exprIndex === -1);/*#ENDIF*/
217
+ /*#IFDEV*/
218
+ assert(exprIndex === -1);/*#ENDIF*/
160
219
 
161
220
 
162
- /*#IFDEV*/this.verify();/*#ENDIF*/
221
+ /*#IFDEV*/
222
+ this.verify();/*#ENDIF*/
163
223
  }
164
224
 
165
225
  /**
@@ -183,14 +243,18 @@ export default class NodeGroup {
183
243
 
184
244
  // Call render() with the same params that would've been passed to the constructor.
185
245
  else if (el.render) {
186
- let oldHash = Globals.componentHash.get(el);
187
- if (oldHash !== newHash)
188
- 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
+ }
189
253
  }
190
254
 
191
- Globals.componentHash.set(el, newHash);
255
+ Globals.componentArgsHash.set(el, newHash);
192
256
  }
193
-
257
+
194
258
  /**
195
259
  * We swap the placeholder element for the real element so we can pass its dynamic attributes
196
260
  * to its constructor.
@@ -204,72 +268,48 @@ export default class NodeGroup {
204
268
  createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
205
269
  if (isPreHtmlElement === undefined)
206
270
  isPreHtmlElement = !el.hasAttribute('_is');
207
-
271
+
208
272
  let tagName = (isPreHtmlElement
209
- ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
210
- ? el.tagName.slice(0, -21)
211
- : el.tagName
273
+ ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
212
274
  : el.getAttribute('is')).toLowerCase();
213
275
 
214
- let dynamicProps = {...(props || {})}
215
-
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
+
216
286
  // Pass other attribs to constructor, since otherwise they're not yet set on the element,
217
287
  // and the constructor would otherwise have no way to see them.
218
288
  if (el.attributes.length) {
219
- if (!props)
220
- props = {};
221
- for (let attrib of el.attributes)
222
- if (!props.hasOwnProperty(attrib.name))
223
- 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
+ }
224
294
  }
225
-
226
- // Create CustomElement and
227
- let Constructor = customElements.get(tagName);
228
- if (!Constructor)
229
- throw new Error(`The custom tag name ${tagName} is not registered.`)
230
295
 
231
- // We pass the childNodes to the constructor so it can know about them,
232
- // instead of only afterward when they're appended to the slot below.
233
- // This is useful for a custom selectbox, for example.
234
- // Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
235
- // can add them as children before the rest of the constructor code executes.
236
- let ch = [... el.childNodes];
237
- Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
238
- 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);
239
300
 
240
301
  if (!isPreHtmlElement)
241
- 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.
242
305
  el.replaceWith(newEl);
243
306
 
244
- // Set children / slot children
245
- // TODO: Match named slots.
246
- // TODO: This only appends to slot if render() is called in the constructor.
247
- //let slot = newEl.querySelector('slot') || newEl;
248
- //slot.append(...el.childNodes);
249
-
250
- // Copy over event attributes.
251
- for (let propName in props) {
252
- let val = props[propName];
253
- if (propName.startsWith('on') && typeof val === 'function')
254
- newEl.addEventListener(propName.slice(2), e => val(e, newEl));
255
-
256
- // Bind array based event attributes on value.
257
- // This same logic is in ExprPath.applyValueAttrib() for non-components.
258
- if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
259
- let [obj, path] = [val[0], val.slice(1)];
260
- newEl.value = delve(obj, path);
261
- newEl.addEventListener('input', e => {
262
- delve(obj, path, Util.getInputValue(newEl));
263
- }, true); // We use capture so we update the values before other events added by the user.
264
- }
265
- }
266
-
267
307
  // If an id pointed at the placeholder, update it to point to the new element.
268
308
  let id = el.getAttribute('data-id') || el.getAttribute('id');
269
309
  if (id)
270
310
  delve(this.getRootNode(), id.split(/\./g), newEl);
271
-
272
-
311
+
312
+
273
313
  // Update paths to use replaced element.
274
314
  for (let path of this.paths) {
275
315
  if (path.nodeMarker === el)
@@ -281,31 +321,31 @@ export default class NodeGroup {
281
321
  this.startNode = newEl;
282
322
  if (this.endNode === el)
283
323
  this.endNode = newEl;
284
-
285
-
324
+
325
+
286
326
  // applyComponentExprs() is called because we're rendering.
287
327
  // So we want to render the sub-component also.
288
328
  if (newEl.renderFirstTime)
289
329
  newEl.renderFirstTime();
290
-
330
+
291
331
  // Copy attributes over.
292
332
  for (let attrib of el.attributes)
293
333
  if (attrib.name !== '_is')
294
334
  newEl.setAttribute(attrib.name, attrib.value);
295
335
 
296
336
  // Set dynamic attributes if they are primitive types.
297
- for (let name in dynamicProps) {
298
- let val = dynamicProps[name];
337
+ for (let name in props) {
338
+ let val = props[name];
299
339
  if (typeof val === 'boolean') {
300
340
  if (val !== false && val !== undefined && val !== null)
301
341
  newEl.setAttribute(name, '');
302
342
  }
303
343
 
304
- // If type isn't an object or array, set the attribute.
344
+ // If type is a non-boolean primitive, set the attribute value.
305
345
  else if (['number', 'bigint', 'string'].includes(typeof val))
306
346
  newEl.setAttribute(name, val);
307
347
  }
308
-
348
+
309
349
  return newEl;
310
350
  }
311
351
 
@@ -349,11 +389,21 @@ export default class NodeGroup {
349
389
  return this.rootNg;
350
390
  }
351
391
 
392
+ /**
393
+ * Requires the nodeCache to be present. */
394
+ removeAndSaveOrphans() {
395
+ /*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
396
+ let fragment = document.createDocumentFragment();
397
+ for (let node of this.getNodes())
398
+ fragment.append(node);
399
+ }
400
+
352
401
 
353
402
  updatePaths(fragment, paths, offset) {
354
403
  // Update paths to point to the fragment.
355
- this.paths.length = paths.length;
356
- 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++) {
357
407
  let path = paths[i].clone(fragment, offset)
358
408
  path.parentNg = this;
359
409
  this.paths[i] = path;
@@ -375,23 +425,23 @@ export default class NodeGroup {
375
425
  * An interleaved array of sets of nodes and top-level ExprPaths
376
426
  * @type {(Node|HTMLElement|ExprPath)[]} */
377
427
  get nodes() { throw new Error('')};
378
-
428
+
379
429
  get debug() {
380
430
  return [
381
431
  `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
382
432
  'nodes:',
383
433
  ...setIndent(this.getNodes().map(item => {
384
434
  if (item instanceof Node) {
385
-
435
+
386
436
  let tree = nodeToArrayTree(item, nextNode => {
387
-
388
- 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));
389
439
  if (path)
390
440
  return [`Path.nodes:`]
391
-
441
+
392
442
  return [];
393
443
  })
394
-
444
+
395
445
  // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
396
446
  return flattenAndIndent(tree)
397
447
  }
@@ -402,10 +452,10 @@ export default class NodeGroup {
402
452
  }
403
453
 
404
454
  get debugNodes() { return this.getNodes() }
405
-
406
-
455
+
456
+
407
457
  get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
408
-
458
+
409
459
  verify() {
410
460
  if (!window.verify)
411
461
  return;
@@ -420,7 +470,7 @@ export default class NodeGroup {
420
470
 
421
471
  // if (this.parentPath)
422
472
  // assert(this.parentPath.nodeGroups.includes(this));
423
-
473
+
424
474
  for (let path of this.paths) {
425
475
  assert(path.parentNg === this)
426
476
 
@@ -436,61 +486,70 @@ export default class NodeGroup {
436
486
  }
437
487
  //#ENDIF
438
488
 
489
+ findStaticComponents(root, shell, pathOffset=0) {
490
+ let result = [];
439
491
 
440
- /**
441
- * @param root {HTMLElement}
442
- * @param shell {Shell}
443
- * @param pathOffset {int} */
444
- activateEmbeds(root, shell, pathOffset=0) {
445
-
446
- // static components. These are WebComponents not created by an expression.
447
- // 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.
448
496
  for (let path of shell.staticComponents) {
449
497
  if (pathOffset)
450
498
  path = path.slice(0, -pathOffset);
451
499
  let el = resolveNodePath(root, path);
452
500
 
453
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.
454
503
  if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
455
- this.createNewComponent(el)
504
+ result.push(el);
456
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) {
457
519
 
458
520
  let rootEl = this.rootNg.root;
459
521
  if (rootEl) {
522
+ let options = this.rootNg.options;
460
523
 
461
524
  // ids
462
- if (this.options?.ids !== false)
525
+ if (options?.ids !== false) {
463
526
  for (let path of shell.ids) {
464
527
  if (pathOffset)
465
528
  path = path.slice(0, -pathOffset);
466
529
  let el = resolveNodePath(root, path);
467
- let id = el.getAttribute('data-id') || el.getAttribute('id');
468
- if (id) { // If something hasn't removed the id.
469
-
470
- // Don't allow overwriting existing class properties if they already have a non-Node value.
471
- if (rootEl[id] && !(rootEl[id] instanceof Node))
472
- throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
473
- `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
474
-
475
- delve(rootEl, id.split(/\./g), el);
476
- }
530
+ Util.bindId(rootEl, el);
531
+ }
477
532
  }
478
533
 
479
534
  // styles
480
- if (this.options?.styles !== false) {
535
+ if (options?.styles !== false) {
481
536
  if (shell.styles.length)
482
537
  this.styles = new Map();
483
538
  for (let path of shell.styles) {
484
539
  if (pathOffset)
485
540
  path = path.slice(0, -pathOffset);
541
+
542
+ /** @type {HTMLStyleElement} */
486
543
  let style = resolveNodePath(root, path);
487
- Util.bindStyles(style, rootEl);
488
- this.styles.set(style, style.textContent);
544
+ if (rootEl.nodeType === 1) {
545
+ Util.bindStyles(style, rootEl);
546
+ this.styles.set(style, style.textContent);
547
+ }
489
548
  }
490
549
 
491
550
  }
492
551
  // scripts
493
- if (this.options?.scripts !== false) {
552
+ if (options?.scripts !== false) {
494
553
  for (let path of shell.scripts) {
495
554
  if (pathOffset)
496
555
  path = path.slice(0, -pathOffset);
@@ -510,6 +569,11 @@ export class RootNodeGroup extends NodeGroup {
510
569
  * @type {HTMLElement} */
511
570
  root;
512
571
 
572
+ /**
573
+ * When we call renerWatched() we re-render these expressions, then clear this to a new Map()
574
+ * @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
575
+ exprsToRender = new Map();
576
+
513
577
  /**
514
578
  *
515
579
  * @param template
@@ -524,73 +588,102 @@ export class RootNodeGroup extends NodeGroup {
524
588
  this.rootNg = this;
525
589
  let [fragment, shell] = this.init(template);
526
590
 
527
- // If adding NodeGroup to an element.
528
- let offset = 0;
529
- let root = fragment; // TODO: Rename so it's not confused with this.root.
530
- if (el) {
591
+ if (fragment instanceof Text) {
531
592
 
532
- // Save slot children
533
- let slotFragment;
534
- if (el.childNodes.length) {
535
- slotFragment = document.createDocumentFragment();
536
- slotFragment.append(...el.childNodes);
593
+ if (el) {
594
+ this.startNode = el;
595
+ this.endNode = el;
596
+ if (fragment.nodeValue.length)
597
+ el.append(fragment);
598
+ this.root = el;
537
599
  }
600
+ Globals.nodeGroups.set(this.root, this);
601
+ }
602
+ else {
538
603
 
539
- 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
+ }
540
616
 
541
- // If el should replace the root node of the fragment.
542
- if (isReplaceEl(fragment, el)) {
543
- el.append(...fragment.children[0].childNodes);
617
+ this.root = el;
544
618
 
545
- // Copy attributes
546
- for (let attrib of fragment.children[0].attributes)
547
- if (!el.hasAttribute(attrib.name))
548
- 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);
549
622
 
550
- // Go one level deeper into all of shell's paths.
551
- offset = 1;
552
- }
553
- else {
554
- let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
555
- if (!isEmpty)
556
- el.append(...fragment.childNodes);
557
- }
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) {
558
638
 
559
- // Setup slots
560
- if (slotFragment) {
561
- for (let slot of el.querySelectorAll('slot[name]')) {
562
- let name = slot.getAttribute('name')
563
- if (name) {
564
- let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
565
- 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
+ }
566
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);
567
656
  }
568
- let unamedSlot = el.querySelector('slot:not([name])')
569
- if (unamedSlot)
570
- unamedSlot.append(slotFragment);
571
- else
572
- el.append(slotFragment);
573
- }
574
657
 
575
- root = el;
576
- this.startNode = el;
577
- this.endNode = el;
578
- }
579
- else {
580
- let singleEl = getSingleEl(fragment);
581
- this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
582
- if (singleEl) {
583
- root = singleEl;
584
- offset = 1;
658
+ root = el;
659
+
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.
665
+
666
+ Globals.nodeGroups.set(this.root, this);
667
+ if (singleEl) {
668
+ root = singleEl;
669
+ offset = 1;
670
+ }
585
671
  }
586
- }
587
672
 
588
- this.updatePaths(root, shell.paths, offset);
673
+ this.updatePaths(root, shell.paths, offset);
674
+
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);
679
+
680
+ this.activateEmbeds(root, shell, offset);
589
681
 
590
- this.activateEmbeds(root, shell, offset);
682
+ // Apply exprs
683
+ this.applyExprs(template.exprs);
591
684
 
592
- // Apply exprs
593
- this.applyExprs(template.exprs);
685
+ this.activateStaticComponents(staticComponents);
686
+ }
594
687
  }
595
688
  }
596
689
 
@@ -612,7 +705,7 @@ function getSingleEl(fragment) {
612
705
  * @param el {HTMLElement}
613
706
  * @returns {boolean} */
614
707
  function isReplaceEl(fragment, el) {
615
- return el.tagName.includes('-')
616
- && fragment.children.length===1
708
+ return fragment.children.length===1
709
+ && el.tagName.includes('-')
617
710
  && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
618
711
  }