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.
package/dist/Solarite.js CHANGED
@@ -76,19 +76,112 @@ var Util$1 = {
76
76
 
77
77
  return result;
78
78
  },
79
-
80
-
81
79
 
80
+ /**
81
+ * Use an array as the value of a map, appending to it when we add.
82
+ * Used by watch.js.
83
+ * @param map {Map|WeakMap|Object}
84
+ * @param key
85
+ * @param value */
86
+ mapArrayAdd(map, key, value) {
87
+ let result = map.get(key);
88
+ if (!result) {
89
+ result = [value];
90
+ map.set(key, result);
91
+ }
92
+ else
93
+ result.push(value);
94
+ },
82
95
  };
83
96
 
97
+ var Globals;
98
+
99
+ /**
100
+ * Created with a reset() function because it's useful for testing. */
101
+ function reset() {
102
+ Globals = {
103
+
104
+ /**
105
+ * Used by NodeGroup.applyComponentExprs() */
106
+ componentArgsHash: new WeakMap(),
107
+
108
+ /**
109
+ * Store which instances of Solarite have already been added to the DOM.
110
+ * @type {WeakSet<HTMLElement>} */
111
+ connected: new WeakSet(),
112
+
113
+ /**
114
+ * ExprPath.applyExactNodes() sets this property when an expression is being accessed.
115
+ * watch() then adds the ExprPath to the list of ExprPaths that should be re-rendered when the value changes.
116
+ * @type {ExprPath}*/
117
+ currentExprPath: null,
118
+
119
+ div: document.createElement("div"),
120
+
121
+ /**
122
+ * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
123
+ elementClasses: {},
124
+
125
+ /** @type {Object<string, boolean>} Key is tag-name.propName. Value is whether it's an attribute.*/
126
+ htmlProps: {},
127
+
128
+ /**
129
+ * Used by ExprPath.applyEventAttrib()
130
+ * @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
131
+ nodeEvents: new WeakMap(),
132
+
133
+ /**
134
+ * Get the RootNodeGroup for an element.
135
+ * @type {WeakMap<HTMLElement, RootNodeGroup>} */
136
+ nodeGroups: new WeakMap(),
137
+
138
+ /**
139
+ * Used by r() path 9. */
140
+ objToEl: new WeakMap(),
141
+
142
+ //pendingChildren: [],
143
+
144
+
145
+ /**
146
+ * Elements that have been rendered to by r() at least once.
147
+ * This is used by the Solarite class to know when to call onFirstConnect()
148
+ * @type {WeakSet<HTMLElement>} */
149
+ rendered: new WeakSet(),
150
+
151
+ /**
152
+ * Elements that are currently rendering via the r() function.
153
+ * @type {WeakSet<HTMLElement>} */
154
+ rendering: new WeakSet(),
155
+
156
+ /**
157
+ * Map from array of Html strings to a Shell created from them.
158
+ * @type {WeakMap<string[], Shell>} */
159
+ shells: new WeakMap(),
160
+
161
+ /**
162
+ * A map of individual untagged strings to their Templates.
163
+ * This way we don't keep creating new Templates for the same string when re-rendering.
164
+ * This is used by ExprPath.applyExactNodes()
165
+ * @type {Object<string, Template>} */
166
+ //stringTemplates: {},
167
+
168
+ reset,
169
+
170
+ count: 0
171
+ };
172
+ }
173
+ reset();
174
+
175
+ var Globals$1 = Globals;
176
+
84
177
  /**
85
178
  * Follow a path into an object.
86
179
  * @param obj {object}
87
180
  * @param path {string[]}
88
- * @param createVal {*} If set, non-existant paths will be created and value at path will be set to createVal.
181
+ * @param createVal {*} If set, non-existent paths will be created and value at path will be set to createVal.
89
182
  * @return {*} The value, or undefined if it can't be reached. */
90
- function delve(obj, path, createVal = delveDontCreate) {
91
- let isCreate = createVal !== delveDontCreate;
183
+ function delve(obj, path, createVal = d) {
184
+ let isCreate = createVal !== d;
92
185
 
93
186
  let len = path.length;
94
187
  if (!obj && !isCreate && len)
@@ -123,245 +216,27 @@ function delve(obj, path, createVal = delveDontCreate) {
123
216
  return obj;
124
217
  }
125
218
 
126
- let delveDontCreate = {};
219
+ // d means "don't create"
220
+ let d = {};
127
221
 
128
- /**
129
- * There are three ways to create an instance of a Solarite Component:
130
- * 1. new ComponentName(); // direct class instantiation
131
- * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another Component.
132
- * 3. <body><component-name></component-name></body> // in the Document html.
133
- *
134
- * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
135
- * sure we get the correct value via all three paths, we write our constructors according to the following
136
- * example. Note that constructor args are embedded in an object, and must be all lower-case because
137
- * Browsers make all html attribute names lowercase.
138
- *
139
- * @example
140
- * constructor({name, userid=1}={}) {
141
- * super();
142
- *
143
- * // Get value from "name" attriute if persent, otherwise from name constructor arg.
144
- * this.name = getArg(this, 'name', name);
145
- *
146
- * // Optionally convert the value to an integer.
147
- * this.userId = getArg(this, 'userid', userid, ArgType.Int);
148
- * }
149
- *
150
- * @param el {HTMLElement}
151
- * @param name {string} Attribute name. Not case-sensitive.
152
- * @param val {*} Default value to use if attribute doesn't exist.
153
- * @param type {ArgType|function|*[]}
154
- * If an array, use the value if it's in the array, otherwise return undefined.
155
- * If it's a function, pass the value to the function and return the result.
156
- * @return {*} */
157
- function getArg(el, name, val=null, type=ArgType.String) {
158
- let attrVal = el.getAttribute(name);
159
- if (attrVal !== null) // If attribute doesn't exist.
160
- val = attrVal;
161
-
162
- if (Array.isArray(type))
163
- return type.includes(val) ? val : undefined;
164
-
165
- if (typeof type === 'function')
166
- return type(val);
167
-
168
- // If bool, it's true as long as it exists and its value isn't falsey.
169
- if (type===ArgType.Bool) {
170
- let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
171
- return !['false', '0', false, 0, null, undefined].includes(lAttrVal);
172
- }
173
-
174
- // Attribute doesn't exist
175
- switch (type) {
176
- case ArgType.Int:
177
- return parseInt(val);
178
- case ArgType.Float:
179
- return parseFloat(val);
180
- case ArgType.String:
181
- return [undefined, null, false].includes(val) ? '' : val+'';
182
- case ArgType.JSON:
183
- case ArgType.Eval:
184
- if (typeof val === 'string' && val.length)
185
- try {
186
- if (type === ArgType.JSON)
187
- return JSON.parse(val);
188
- else
189
- return eval(`(${val})`);
190
- } catch (e) {
191
- return val;
192
- }
193
- else return val;
194
- default:
195
- return val;
196
- }
197
- }
198
-
199
- /**
200
- * @enum */
201
- var ArgType = {
202
-
203
- /**
204
- * false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
205
- * Anything else, including empty string becomes true.
206
- * Empty string is true because attributes with no value should be evaulated as true. */
207
- Bool: 'Bool',
208
-
209
- Int: 'Int',
210
- Float: 'Float',
211
- String: 'String',
212
-
213
- /**
214
- * Parse the string value as JSON.
215
- * If it's not parsable, return the value as a string. */
216
- JSON: 'JSON',
217
-
218
- /**
219
- * Evaluate the string as JavaScript using the eval() function.
220
- * If it can't be evaluated, return the original string. */
221
- Eval: 'Eval'
222
- };
223
-
224
- let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
225
- let objectIds = new WeakMap();
226
-
227
- /**
228
- * @param obj {Object|string|Node}
229
- * @returns {string} */
230
- function getObjectId(obj) {
231
- // if (typeof obj === 'function')
232
- // return obj.toString(); // This fails to detect when a function's bound variables changes.
233
-
234
- let result = objectIds.get(obj);
235
- if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
236
- result = (lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
237
- objectIds.set(obj, result);
238
- }
239
- return result;
240
- }
241
-
242
- /**
243
- * Control how JSON.stringify() handles Nodes and Functions.
244
- * Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
245
- * But that makes JSON.stringify() take twice as long to run.
246
- * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
247
- let isHashing = true;
248
- function toJSON() {
249
- return isHashing ? getObjectId(this) : this
250
- }
251
-
252
-
253
- // Node.prototype.toJSON = toJSON;
254
- // Function.prototype.toJSON = toJSON;
255
- // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
256
- // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
257
- // So we check the assignments on every run of getObjectHash()
258
- if (Node.prototype.toJSON !== toJSON) {
259
- Node.prototype.toJSON = toJSON;
260
- if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
261
- Function.prototype.toJSON = toJSON;
262
- }
222
+ let Util = {
263
223
 
264
- /**
265
- * Get a string that uniquely maps to the values of the given object.
266
- * If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
267
- * This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
268
- *
269
- * Relies on the Node and Function prototypes being overridden above.
270
- *
271
- * Note that passing an integer may collide with the number we get from hashing an object.
272
- * But we don't handle that case because we need max performance and Solarite never passes integers to this function.
273
- *
274
- * @param obj {*}
275
- * @returns {string} */
276
- function getObjectHash(obj) {
277
- let result;
278
- isHashing = true;
279
- try {
280
- result = JSON.stringify(obj);
281
- }
282
- catch(e) {
283
- result = getObjectHashCircular(obj);
284
- }
285
- isHashing = false;
286
- return result;
287
- }
224
+ bindId(root, el) {
225
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
226
+ if (id) { // If something hasn't removed the id.
288
227
 
289
- /**
290
- * Slower hashing method that supports.
291
- * @param obj
292
- * @returns {string} */
293
- function getObjectHashCircular(obj) {
228
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
229
+ if (root[id] && !(root[id] instanceof Node))
230
+ throw new Error(`${root.constructor.name}.${id} already has a value. ` +
231
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
294
232
 
295
- //console.log('circular')
296
- // Slower version that handles circular references.
297
- // Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
298
- const seen = new Set();
299
- return JSON.stringify(obj, (key, value) => {
300
- if (typeof value === 'object' && value !== null) {
301
- if (seen.has(value))
302
- return getObjectId(value);
303
- seen.add(value);
233
+ delve(root, id.split(/\./g), el);
304
234
  }
305
- return value;
306
- });
307
- }
308
-
309
-
310
-
311
- var Globals = {
312
-
313
- /**
314
- * Used by NodeGroup.applyComponentExprs() */
315
- componentHash: new WeakMap(),
316
-
317
- /**
318
- * Store which instances of Solarite have already been added to the DOM.
319
- * @type {WeakSet<HTMLElement>} */
320
- connected: new WeakSet(),
321
-
322
- /**
323
- * Elements that have been rendered to by r() at least once.
324
- * This is used by the Solarite class to know when to call onFirstConnect()
325
- * @type {WeakSet<HTMLElement>} */
326
- rendered: new WeakSet(),
327
-
328
- /**
329
- * Used by watch3 to see which expressions are being accessed. */
330
- currentExprPath: [],
331
-
332
- /**
333
- * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
334
- elementClasses: {},
335
-
336
- /**
337
- * Used by ExprPath.applyEventAttrib()
338
- * @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
339
- nodeEvents: new WeakMap(),
340
-
341
- /**
342
- * Get the RootNodeGroup for an element.
343
- * @type {WeakMap<HTMLElement, RootNodeGroup>} */
344
- nodeGroups: new WeakMap(),
345
-
346
- /**
347
- * Used by r() path 9. */
348
- objToEl: new WeakMap(),
349
-
350
- pendingChildren: [],
351
-
352
- /**
353
- * Elements that are currently rendering via the r() function.
354
- * @type {WeakSet<HTMLElement>} */
355
- rendering: new WeakSet(),
235
+ },
356
236
 
357
237
  /**
358
- * Map from array of Html strings to a Shell created from them.
359
- * @type {WeakMap<string[], Shell>} */
360
- shells: new WeakMap()
361
- };
362
-
363
- let Util = {
364
-
238
+ * @param style {HTMLStyleElement}
239
+ * @param root {HTMLElement} */
365
240
  bindStyles(style, root) {
366
241
  let styleId = root.getAttribute('data-style');
367
242
  if (!styleId) {
@@ -374,17 +249,60 @@ let Util = {
374
249
  root.setAttribute('data-style', styleId);
375
250
  }
376
251
 
252
+ // Replace ":host" with "tagName[data-style=...]" in the css.
377
253
  let tagName = root.tagName.toLowerCase();
378
254
  for (let child of style.childNodes) {
379
255
  if (child.nodeType === 3) {
380
256
  let oldText = child.textContent;
381
- let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName + '[data-style="' + styleId + '"]');
257
+ let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`);
382
258
  if (oldText !== newText)
383
259
  child.textContent = newText;
384
260
  }
385
261
  }
386
262
  },
387
263
 
264
+
265
+ /**
266
+ * Convert a Proper Case name to a name with dashes.
267
+ * Dashes will be placed between letters and numbers.
268
+ * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
269
+ * @param str {string}
270
+ * @return {string}
271
+ *
272
+ * @example
273
+ * 'ProperName' => 'proper-name'
274
+ * 'HTMLElement' => 'html-element'
275
+ * 'BigUI' => 'big-ui'
276
+ * 'UIForm' => 'ui-form'
277
+ * 'A100' => 'a-100' */
278
+ camelToDashes(str) {
279
+ // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
280
+ str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
281
+
282
+ // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
283
+ str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
284
+
285
+ // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
286
+ str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
287
+
288
+ // Convert all the remaining capital letters to lowercase.
289
+ return str.toLowerCase();
290
+ },
291
+
292
+ /**
293
+ * Converts a string written in kebab-case to camelCase.
294
+ *
295
+ * @param {string} str - The input string written in kebab-case.
296
+ * @return {string} - The resulting camelCase string.
297
+ *
298
+ * @example
299
+ * dashesToCamel('example-string') // Returns 'exampleString'
300
+ * dashesToCamel('another-example-test') // Returns 'anotherExampleTest' */
301
+ dashesToCamel(str) {
302
+ return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
303
+ },
304
+
305
+
388
306
  /**
389
307
  * A generator function that recursively traverses and flattens a value.
390
308
  *
@@ -428,6 +346,7 @@ let Util = {
428
346
  * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
429
347
  * @return {string|string[]|number|[]|File[]|Date|boolean} */
430
348
  getInputValue(node) {
349
+ // .type is a built-in DOM property
431
350
  if (node.type === 'checkbox' || node.type === 'radio')
432
351
  return node.checked; // Boolean
433
352
  if (node.type === 'file')
@@ -442,48 +361,63 @@ let Util = {
442
361
  return node.value; // String
443
362
  },
444
363
 
364
+ /**
365
+ * @param el {HTMLElement}
366
+ * @param prop {string}
367
+ * @returns {boolean} */
368
+ isHtmlProp(el, prop) {
369
+ let key = el.tagName + '.' + prop;
370
+ let result = Globals$1.htmlProps[key];
371
+ if (result === undefined) { // Caching just barely makes this slightly faster.
372
+ let proto = Object.getPrototypeOf(el);
373
+
374
+ // Find the first HTMLElement that we inherit from (not our own classes)
375
+ while (proto) {
376
+ const ctorName = proto.constructor.name;
377
+ if (ctorName.startsWith('HTML') && ctorName.endsWith('Element'))
378
+ break
379
+ proto = Object.getPrototypeOf(proto);
380
+ }
381
+ Globals$1.htmlProps[key] = result = (proto
382
+ ? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
383
+ : false);
384
+ }
385
+ return result;
386
+ },
387
+
445
388
  /**
446
389
  * Is it an array and a path that can be evaluated by delve() ?
390
+ * We allow the first element to be null/undefined so binding can report errors.
447
391
  * @param arr {Array|*}
448
392
  * @returns {boolean} */
449
393
  isPath(arr) {
450
- return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
394
+ return Array.isArray(arr) && arr.length >=2 // An array of at least two elements.
395
+ && (typeof arr[0] === 'object' || arr[0] === undefined) // Where the first element is an object, null, or undefined.
396
+ && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number'); // Path 1..x is only numbers and strings.
397
+ },
398
+
399
+ isFalsy(val) {
400
+ return val === undefined || val === false || val === null;
401
+ },
402
+
403
+ isPrimitive(val) {
404
+ return typeof val === 'string' || typeof val === 'number'
451
405
  },
452
406
 
453
407
  /**
454
- * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
455
- * they're not lost forever and the NodeGroup's internal structure is still consistent.
456
- * This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
457
- * This is necessary because a NodeGroup normally only stores the first and last node.
458
- * Called from ExprPath.apply().
459
- * @param oldNodeGroups {NodeGroup[]}
460
- * @param oldNodes {Node[]} */
461
- saveOrphans(oldNodeGroups, oldNodes) {
462
- let oldNgMap = new Map();
463
- for (let ng of oldNodeGroups) {
464
- oldNgMap.set(ng.startNode, ng);
465
-
466
- // TODO: Is this necessary?
467
- // if (ng.parentPath)
468
- // ng.parentPath.clearNodesCache();
469
- }
470
-
471
- for (let i=0, node; node = oldNodes[i]; i++) {
472
- let ng;
473
- if (!node.parentNode && (ng = oldNgMap.get(node))) {
474
- //ng.nodesCache = [];
475
- let fragment = document.createDocumentFragment();
476
- let endNode = ng.endNode;
477
- while (node !== endNode) {
478
- fragment.append(node);
479
- //ng.nodesCache.push(node);
480
- i++;
481
- node = oldNodes[i];
482
- }
483
- fragment.append(endNode);
484
- //ng.nodesCache.push(endNode);
485
- }
486
- }
408
+ * If val is a function, evaluate it recursively until the result is not a function.
409
+ * If it's an array or an object, convert it to Json.
410
+ * If it's a Date, format it as Y-m-d H:i:s
411
+ * @param val
412
+ * @returns {string|number|boolean} */
413
+ makePrimitive(val) {
414
+ if (typeof val === 'function')
415
+ return Util.makePrimitive(val());
416
+ else if (val instanceof Date)
417
+ return val.toISOString().replace(/T/, ' ');
418
+ else if (Array.isArray(val) || typeof val === 'object')
419
+ return ''; // JSON.stringify(val);
420
+ return val;
487
421
  },
488
422
 
489
423
  /**
@@ -514,136 +448,272 @@ let Util = {
514
448
 
515
449
 
516
450
 
517
- let div = document.createElement('div');
451
+ let isEvent = attrName => attrName.startsWith('on') && attrName in Globals$1.div;
452
+
453
+
454
+
455
+
456
+
457
+ /**
458
+ * Returns true if they're the same.
459
+ * @param a
460
+ * @param b
461
+ * @returns {boolean} */
462
+ function arraySame(a, b) {
463
+ let aLength = a.length;
464
+ if (aLength !== b.length)
465
+ return false;
466
+ for (let i=0; i<aLength; i++)
467
+ if (a[i] !== b[i])
468
+ return false;
469
+ return true; // the same.
470
+ }
471
+
472
+
473
+
474
+
475
+
476
+ // For debugging only
477
+
478
+
479
+ function defineClass(Class, tagName, extendsTag) {
480
+ if (!customElements[getName](Class)) { // If not previously defined.
481
+ tagName = tagName || Util.camelToDashes(Class.name);
482
+ if (!tagName.includes('-'))
483
+ tagName += '-element';
484
+
485
+ let options = null;
486
+ if (extendsTag)
487
+ options = {extends: extendsTag};
488
+
489
+ customElements[define](tagName, Class, options);
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Create a version of the Solarite class that extends from the given tag name.
495
+ * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
496
+ * 1. customElements.define() is called automatically when you create the first instance.
497
+ * 2. Calls render() when added to the DOM, if it hasn't been called already.
498
+ * 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
499
+ * 4. We can use this.html = r`...` to set html. (deprecated)
500
+ * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
501
+ * Can't figure out how to have these work standalone though, and still be synchronous.
502
+ * 6. Can we extend from other element types like TR?
503
+ * 7. Shows default text if render() function isn't defined.
504
+ *
505
+ * Advantages to inheriting from HTMLElement
506
+ * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
507
+ * 2. We can inherit from things like HTMLTableRowElement directly.
508
+ * 3. There's less magic, since everyone is familiar with defining custom elements.
509
+ *
510
+ * @param extendsTag {?string}
511
+ * @return {Class} */
512
+ function createSolarite(extendsTag=null) {
513
+
514
+ let BaseClass = HTMLElement;
515
+ if (extendsTag && !extendsTag.includes('-')) {
516
+ extendsTag = extendsTag.toLowerCase();
517
+
518
+ BaseClass = Globals$1.elementClasses[extendsTag];
519
+ if (!BaseClass) { // TODO: Use Cache
520
+ BaseClass = document.createElement(extendsTag).constructor;
521
+ Globals$1.elementClasses[extendsTag] = BaseClass;
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Intercept the construct call to auto-define the class before the constructor is called.
527
+ * @type {HTMLElement} */
528
+ let HTMLElementAutoDefine = new Proxy(BaseClass, {
529
+ construct(Parent, args, Class) {
530
+ defineClass(Class, null, extendsTag);
531
+
532
+ // This is a good place to manipulate any args before they're sent to the constructor.
533
+ // Such as loading them from attributes, if I could find a way to do so.
534
+
535
+ // This line is equivalent the to super() call.
536
+ return Reflect.construct(Parent, args, Class);
537
+ }
538
+ });
539
+
540
+ return class Solarite extends HTMLElementAutoDefine {
541
+
542
+
543
+ /**
544
+ * TODO: Make these standalone functions.
545
+ * Callbacks.
546
+ * Use onConnect.push(() => ...); to add new callbacks. */
547
+ onConnect = Util$1.callback();
548
+
549
+ onFirstConnect = Util$1.callback();
550
+ onDisconnect = Util$1.callback();
551
+
552
+ /**
553
+ * @param options {RenderOptions} */
554
+ constructor(options={}) {
555
+ super();
556
+
557
+ // TODO: Is options.render ever used?
558
+ if (options.render===true)
559
+ this.render();
560
+
561
+ else if (options.render===false)
562
+ Globals$1.rendered.add(this); // Don't render on connectedCallback()
563
+
564
+ // Add slot children before constructor code executes.
565
+ // This breaks the styleStaticNested test.
566
+ // PendingChildren is setup in NodeGroup.createNewComponent()
567
+ // TODO: Match named slots.
568
+ //let ch = Globals.pendingChildren.pop();
569
+ //if (ch) // TODO: how could there be a slot before render is called?
570
+ // (this.querySelector('slot') || this).append(...ch);
518
571
 
519
- let isEvent = attrName => attrName.startsWith('on') && attrName in div;
572
+ /** @deprecated
573
+ Object.defineProperty(this, 'html', {
574
+ set(html) {
575
+ Globals.rendered.add(this);
576
+ if (typeof html === 'string') {
577
+ console.warn("Assigning to this.html without the r template prefix.")
578
+ this.innerHTML = html;
579
+ }
580
+ else
581
+ this.modifications = r(this, html, options);
582
+ }
583
+ })*/
520
584
 
585
+ /*
586
+ let pthis = new Proxy(this, {
587
+ get(obj, prop) {
588
+ return Reflect.get(obj, prop)
589
+ }
590
+ });
591
+ this.render = this.render.bind(pthis);
592
+ */
593
+ }
521
594
 
522
- /**
523
- * Convert a Proper Case name to a name with dashes.
524
- * Dashes will be placed between letters and numbers.
525
- * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
526
- * @param str {string}
527
- * @return {string}
528
- *
529
- * @example
530
- * 'ProperName' => 'proper-name'
531
- * 'HTMLElement' => 'html-element'
532
- * 'BigUI' => 'big-ui'
533
- * 'UIForm' => 'ui-form'
534
- * 'A100' => 'a-100' */
535
- function camelToDashes(str) {
536
- // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
537
- str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
538
-
539
- // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
540
- str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
541
-
542
- // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
543
- str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
544
-
545
- // Convert all the remaining capital letters to lowercase.
546
- return str.toLowerCase();
547
- }
595
+ /**
596
+ * Call render() only if it hasn't already been called. */
597
+ renderFirstTime() {
598
+ if (!Globals$1.rendered.has(this) && this.render)
599
+ this.render();
600
+ }
601
+
602
+ /**
603
+ * Called automatically by the browser. */
604
+ connectedCallback() {
605
+ this.renderFirstTime();
606
+ if (!Globals$1.connected.has(this)) {
607
+ Globals$1.connected.add(this);
608
+ this.onFirstConnect();
609
+ }
610
+ this.onConnect();
611
+ }
612
+
613
+ disconnectedCallback() {
614
+ this.onDisconnect();
615
+ }
548
616
 
549
617
 
618
+ static define(tagName=null) {
619
+ defineClass(this, tagName, extendsTag);
620
+ }
621
+ }
622
+ }
550
623
 
624
+ // Trick to prevent minifier from renaming this method.
625
+ let define = 'define';
626
+ let getName = 'getName';
627
+
628
+
629
+
630
+ let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
631
+ let objectIds = new WeakMap();
551
632
 
633
+ /**
634
+ * @param obj {Object|string|Node}
635
+ * @returns {string} */
636
+ function getObjectId(obj) {
637
+ // if (typeof obj === 'function')
638
+ // return obj.toString(); // This fails to detect when a function's bound variables changes.
639
+
640
+ let result = objectIds.get(obj);
641
+ if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
642
+ result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
643
+ objectIds.set(obj, result);
644
+ }
645
+ return result;
646
+ }
552
647
 
553
648
  /**
554
- * Returns false if they're the same. Or the first index where they differ.
555
- * @param a
556
- * @param b
557
- * @returns {boolean} */
558
- function arraySame(a, b) {
559
- let aLength = a.length;
560
- if (aLength !== b.length)
561
- return false;
562
- for (let i=0; i<aLength; i++)
563
- if (a[i] !== b[i])
564
- return false;
565
- return true; // the same.
649
+ * Control how JSON.stringify() handles Nodes and Functions.
650
+ * Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
651
+ * But that makes JSON.stringify() take twice as long to run.
652
+ * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
653
+ let isHashing = true;
654
+ function toJSON() {
655
+ return isHashing ? getObjectId(this) : this
566
656
  }
567
657
 
568
658
 
659
+ // Node.prototype.toJSON = toJSON;
660
+ // Function.prototype.toJSON = toJSON;
661
+
662
+
569
663
  /**
570
- * TODO: Turn this into a class because it has internal state.
571
- * TODO: Don't break on 3<a inside a <script> or <style> tag.
572
- * @param html {?string} Pass null to reset context.
664
+ * Get a string that uniquely maps to the values of the given object.
665
+ * If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
666
+ * This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
667
+ *
668
+ * Relies on the Node and Function prototypes being overridden above.
669
+ *
670
+ * Note that passing an integer may collide with the number we get from hashing an object.
671
+ * But we don't handle that case because we need max performance and Solarite never passes integers to this function.
672
+ *
673
+ * @param obj {*}
573
674
  * @returns {string} */
574
- function htmlContext(html) {
575
- if (html === null) {
576
- state = {...defaultState};
577
- return state.context;
578
- }
579
- for (let i = 0; i < html.length; i++) {
580
- const char = html[i];
581
- switch (state.context) {
582
- case htmlContext.Text:
583
- if (char === '<' && html[i+1].match(/[a-z!]/i)) { // Start of a tag or comment.
584
- // if (html.slice(i, i+4) === '<!--')
585
- // state.context = htmlContext.Comment;
586
- // else
587
- state.context = htmlContext.Tag;
588
- state.buffer = '';
589
- }
590
- break;
591
- case htmlContext.Tag:
592
- if (char === '>') {
593
- state.context = htmlContext.Text;
594
- state.quote = null;
595
- state.buffer = '';
596
- } else if (char === ' ' && !state.buffer) {
597
- // No attribute name is present. Skipping the space.
598
- continue;
599
- } else if (char === ' ' || char === '/' || char === '?') {
600
- state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
601
- } else if (char === '"' || char === "'" || char === '=') {
602
- state.context = htmlContext.Attribute;
603
- state.quote = char === '=' ? null : char;
604
- state.buffer = '';
605
- } else {
606
- state.buffer += char;
607
- }
608
- break;
609
- case htmlContext.Attribute:
610
- if (!state.quote && !state.buffer.length && (char === '"' || char === "'"))
611
- state.quote = char;
612
-
613
- else if (char === state.quote || (!state.quote && state.buffer.length)) {
614
- state.context = htmlContext.Tag;
615
- state.quote = null;
616
- state.buffer = '';
617
- } else if (!state.quote && char === '>') {
618
- state.context = htmlContext.Text;
619
- state.quote = null;
620
- state.buffer = '';
621
- } else if (char !== ' ') {
622
- state.buffer += char;
623
- }
624
- break;
625
- }
675
+ function getObjectHash(obj) {
626
676
 
677
+ // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
678
+ // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
679
+ // So we check the assignments on every run of getObjectHash()
680
+ if (Node.prototype.toJSON !== toJSON) {
681
+ Node.prototype.toJSON = toJSON;
682
+ if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
683
+ Function.prototype.toJSON = toJSON;
627
684
  }
628
- return state.context;
629
- }
630
-
631
685
 
632
- htmlContext.Attribute = 'Attribute';
633
- htmlContext.Text = 'Text';
634
- htmlContext.Tag = 'Tag';
635
- //htmlContext.Comment = 'Comment';
636
- let defaultState = {
637
- context: htmlContext.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
638
- quote: null, // possible values: null, '"', "'"
639
- buffer: '',
640
- lastChar: null
641
- };
642
- let state = {...defaultState};
686
+ let result;
687
+ isHashing = true;
688
+ try {
689
+ result = JSON.stringify(obj);
690
+ }
691
+ catch(e) {
692
+ result = getObjectHashCircular(obj);
693
+ }
694
+ isHashing = false;
695
+ return result;
696
+ }
643
697
 
698
+ /**
699
+ * Slower hashing method that supports.
700
+ * @param obj
701
+ * @returns {string} */
702
+ function getObjectHashCircular(obj) {
644
703
 
645
- // For debugging only
646
-
704
+ //console.log('circular')
705
+ // Slower version that handles circular references.
706
+ // Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
707
+ const seen = new Set();
708
+ return JSON.stringify(obj, (key, value) => {
709
+ if (typeof value === 'object' && value !== null) {
710
+ if (seen.has(value))
711
+ return getObjectId(value);
712
+ seen.add(value);
713
+ }
714
+ return value;
715
+ });
716
+ }
647
717
 
648
718
  class MultiValueMap {
649
719
 
@@ -667,7 +737,10 @@ class MultiValueMap {
667
737
  return false;
668
738
  }
669
739
 
670
- // Get all values for a key
740
+ /**
741
+ * Get all values for a key.
742
+ * @param key {string}
743
+ * @returns {Set|*[]} */
671
744
  getAll(key) {
672
745
  return this.data[key] || [];
673
746
  }
@@ -676,25 +749,16 @@ class MultiValueMap {
676
749
  * Remove one value from a key, and return it.
677
750
  * @param key {string}
678
751
  * @param val If specified, make sure we delete this specific value, if a key exists more than once.
679
- * @returns {*} */
752
+ * @returns {*|undefined} The deleted item. */
680
753
  delete(key, val=undefined) {
681
- // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
682
- // debugger;
683
-
684
754
  let data = this.data;
685
-
686
- // if (!data.hasOwnProperty(key))
687
- // return undefined;
688
-
689
- // Delete a specific value.
690
755
  let result;
691
756
  let set = data[key];
692
- if (!set) // slower than pre-check.
757
+ if (!set)
693
758
  return undefined;
694
759
 
695
760
  // Delete any value.
696
761
  if (val === undefined) {
697
- //result = set.values().next().value; // get first item from set.
698
762
  [result] = set; // Does the same as above and seems to be about the same speed.
699
763
  set.delete(result);
700
764
  }
@@ -705,7 +769,73 @@ class MultiValueMap {
705
769
  result = val;
706
770
  }
707
771
 
708
- // TODO: Will this make it slower?
772
+ if (set.size === 0)
773
+ delete data[key];
774
+
775
+ return result;
776
+ }
777
+
778
+ /**
779
+ * Remove one value from a key, and return it.
780
+ * @param key {string}
781
+ * @returns {*|undefined} The deleted item. */
782
+ deleteAny(key) {
783
+ let data = this.data;
784
+ let result;
785
+ let set = data[key];
786
+ if (!set) // slower than pre-check.
787
+ return undefined;
788
+
789
+ [result] = set; // Does the same as above and seems to be about the same speed.
790
+ set.delete(result);
791
+
792
+ if (set.size === 0)
793
+ delete data[key];
794
+
795
+ return result;
796
+ }
797
+
798
+ deleteSpecific(key, val) {
799
+ let data = this.data;
800
+ let result;
801
+ let set = data[key];
802
+ if (!set)
803
+ return undefined;
804
+
805
+ set.delete(val);
806
+ result = val;
807
+
808
+ if (set.size === 0)
809
+ delete data[key];
810
+
811
+ return result;
812
+ }
813
+
814
+
815
+ /**
816
+ * Try to delete an item that matches the key and the isPreferred function.
817
+ * if not the latter, just delete any item that matches the key.
818
+ * @param key {string}
819
+ * @returns {*|undefined} The deleted item. */
820
+ deletePreferred(key, parent) {
821
+ let result;
822
+ let data = this.data;
823
+ let set = data[key];
824
+ if (!set)
825
+ return undefined;
826
+
827
+ for (let val of set) {
828
+ if (val?.parentNode === parent) {
829
+ set.delete(val);
830
+ result = val;
831
+ break;
832
+ }
833
+ }
834
+ if (!result) {
835
+ [result] = set;
836
+ set.delete(result);
837
+ }
838
+
709
839
  if (set.size === 0)
710
840
  delete data[key];
711
841
 
@@ -912,14 +1042,19 @@ const udomdiff = (parentNode, a, b, before) => {
912
1042
  return b;
913
1043
  };
914
1044
 
1045
+ //import {ArraySpliceOp} from "./watch.js";
1046
+
1047
+
915
1048
  /**
916
1049
  * Path to where an expression should be evaluated within a Shell or NodeGroup.
917
1050
  * Path is only valid until the expressions before it are evaluated.
918
1051
  * TODO: Make this based on parent and node instead of path? */
919
1052
  class ExprPath {
920
1053
 
1054
+
1055
+
921
1056
  /**
922
- * @type {PathType} */
1057
+ * @type {ExprPathType} */
923
1058
  type;
924
1059
 
925
1060
  // Used for attributes:
@@ -935,8 +1070,6 @@ class ExprPath {
935
1070
  * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
936
1071
  attrNames;
937
1072
 
938
-
939
-
940
1073
  /**
941
1074
  * @type {Node} Node that occurs before this ExprPath's first Node.
942
1075
  * This is necessary because udomdiff() can steal nodes from another ExprPath.
@@ -978,13 +1111,23 @@ class ExprPath {
978
1111
  nodeMarkerPath;
979
1112
 
980
1113
 
1114
+ /** @type {?function} A function called by renderWatched() to update the value of this expression. */
1115
+ watchFunction
1116
+
1117
+ /**
1118
+ * @type {?function} The most recent callback passed to a .map() function in this ExprPath.
1119
+ * TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
1120
+ mapCallback
1121
+
1122
+ isHtmlProperty = undefined;
1123
+
981
1124
  /**
982
1125
  * @param nodeBefore {Node}
983
1126
  * @param nodeMarker {?Node}
984
- * @param type {PathType}
1127
+ * @param type {ExprPathType}
985
1128
  * @param attrName {?string}
986
1129
  * @param attrValue {string[]} */
987
- constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
1130
+ constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
988
1131
 
989
1132
  // If path is a node.
990
1133
  this.nodeBefore = nodeBefore;
@@ -992,7 +1135,7 @@ class ExprPath {
992
1135
  this.type = type;
993
1136
  this.attrName = attrName;
994
1137
  this.attrValue = attrValue;
995
- if (type === PathType.Multiple)
1138
+ if (type === ExprPathType.AttribMultiple)
996
1139
  this.attrNames = new Set();
997
1140
  }
998
1141
 
@@ -1006,36 +1149,27 @@ class ExprPath {
1006
1149
  * We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
1007
1150
  * setAttribute() once all the pieces are in place.
1008
1151
  *
1009
- * @param expr {Expr}
1010
1152
  * @param exprs {Expr[]}
1011
- * @param exprIndex {int}
1012
- * @param componentExprs {object}
1013
- * @returns {int} */
1014
- apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
1153
+ * @param freeNodeGroups {boolean} */
1154
+ apply(exprs, freeNodeGroups=true) {
1015
1155
  switch (this.type) {
1016
1156
  case 1: // PathType.Content:
1017
- this.applyNodes(expr);
1157
+ this.applyNodes(exprs[0], freeNodeGroups);
1018
1158
  break;
1019
1159
  case 2: // PathType.Multiple:
1020
- this.applyMultipleAttribs(this.nodeMarker, expr);
1160
+ this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
1021
1161
  break;
1022
1162
  case 5: // PathType.Comment:
1023
1163
  // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
1024
1164
  break;
1025
1165
  case 6: // PathType.Event:
1026
- this.applyEventAttrib(this.nodeMarker, expr, this.parentNg.rootNg.root);
1166
+ this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
1027
1167
  break;
1028
- default:
1029
- if (this.type === 4 /*PathType.Component*/ && this.nodeMarker !== this.parentNg.rootNg.root)
1030
- componentExprs[this.attrName] = expr;
1031
- else {
1032
- // One attribute value may have multiple expressions. Here we apply them all at once.
1033
- exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
1034
- }
1168
+ default: // TODO: Is this still used? Lots of tests fail without it.
1169
+ // One attribute value may have multiple expressions. Here we apply them all at once.
1170
+ this.applyValueAttrib(this.nodeMarker, exprs);
1035
1171
  break;
1036
1172
  }
1037
-
1038
- return exprIndex;
1039
1173
  }
1040
1174
 
1041
1175
  /**
@@ -1043,10 +1177,17 @@ class ExprPath {
1043
1177
  * Called by applyExprs()
1044
1178
  * This function is recursive, as the functions it calls also call it.
1045
1179
  * @param expr {Expr}
1180
+ * @param freeNodeGroups {boolean}
1046
1181
  * @return {Node[]} New Nodes created. */
1047
- applyNodes(expr) {
1182
+ applyNodes(expr, freeNodeGroups=true) {
1048
1183
  let path = this;
1049
1184
 
1185
+ // This can be done at the beginning or the end of this function.
1186
+ // If at the end, we may get rendering done faster.
1187
+ // But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
1188
+ if (freeNodeGroups)
1189
+ path.freeNodeGroups();
1190
+
1050
1191
 
1051
1192
 
1052
1193
  /** @type {(Node|NodeGroup|Expr)[]} */
@@ -1055,10 +1196,10 @@ class ExprPath {
1055
1196
 
1056
1197
  let secondPass = []; // indices
1057
1198
 
1058
- path.nodeGroups = []; // Reset before applyExact and the code below rebuilds it.
1059
- path.applyExact(expr, newNodes, secondPass);
1199
+ path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
1200
+ path.applyExactNodes(expr, newNodes, secondPass);
1060
1201
 
1061
- this.existingTextNodes = null;
1202
+ //this.existingTextNodes = null;
1062
1203
 
1063
1204
  // TODO: Create an array of old vs Nodes and NodeGroups together.
1064
1205
  // If they're all the same, skip the next steps.
@@ -1111,26 +1252,157 @@ class ExprPath {
1111
1252
  // Rearrange nodes.
1112
1253
  udomdiff(path.nodeMarker.parentNode, oldNodes, newNodes, path.nodeMarker);
1113
1254
 
1114
- Util.saveOrphans(oldNodeGroups, oldNodes);
1255
+ // TODO: Put this in a remove() function of NodeGroup.
1256
+ // Then only run it on the old nodeGroups that were actually removed.
1257
+ //Util.saveOrphans(oldNodeGroups, oldNodes);
1258
+
1259
+ for (let ng of oldNodeGroups)
1260
+ if (!ng.startNode.parentNode)
1261
+ ng.removeAndSaveOrphans();
1262
+ }
1263
+
1264
+
1265
+
1266
+ }
1267
+
1268
+ /**
1269
+ * Used by watch() for inserting/removing/replacing individual loop items.
1270
+ * @param op {ArraySpliceOp} */
1271
+ applyArrayOp(op) {
1272
+
1273
+ // Replace NodeGroups
1274
+ let replaceCount = Math.min(op.deleteCount, op.items.length);
1275
+ let deleteCount = op.deleteCount - replaceCount;
1276
+ for (let i=0; i<replaceCount; i++) {
1277
+ let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
1278
+
1279
+ // Try to find an exact match
1280
+ let func = this.mapCallback || this.watchFunction;
1281
+ let expr = func(op.items[i]);
1282
+
1283
+ // If the result of func isn't a template, conver it to one or more templates.
1284
+ this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
1285
+
1286
+ let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1287
+ if (ng && ng === oldNg) ; else {
1288
+
1289
+ // Find a close match or create a new node group
1290
+ if (!ng)
1291
+ ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1292
+ this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
1293
+
1294
+ // Splice in the new nodes.
1295
+ let insertBefore = oldNg.startNode;
1296
+ for (let node of ng.getNodes())
1297
+ insertBefore.parentNode.insertBefore(node, insertBefore);
1298
+
1299
+ // Remove the old nodes.
1300
+ if (ng !== oldNg)
1301
+ oldNg.removeAndSaveOrphans();
1302
+ }
1303
+ });
1304
+ }
1305
+
1306
+ // Delete extra at the end.
1307
+ if (deleteCount > 0) {
1308
+ for (let i=0; i<deleteCount; i++) {
1309
+ let oldNg = this.nodeGroups[op.index + replaceCount + i];
1310
+ oldNg.removeAndSaveOrphans();
1311
+ }
1312
+ this.nodeGroups.splice(op.index + replaceCount, deleteCount);
1115
1313
  }
1116
1314
 
1117
- // Must happen after second pass.
1118
- path.freeNodeGroups();
1315
+ // Add extra at the end.
1316
+ else {
1317
+ let newItems = op.items.slice(replaceCount);
1318
+
1319
+ let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
1320
+ for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
1321
+
1322
+
1323
+ // Try to find exact match
1324
+ let template = this.mapCallback(newItems[i]);
1325
+ let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1326
+ if (!ng) // Find a close match or create a new node group
1327
+ ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1328
+
1329
+ this.nodeGroups.push(ng);
1330
+
1331
+ // Splice in the new nodes.
1332
+ for (let node of ng.getNodes())
1333
+ insertBefore.parentNode.insertBefore(node, insertBefore);
1334
+ }
1335
+ }
1119
1336
 
1120
1337
 
1338
+
1339
+ // TODO: update or invalidate the nodes cache?
1340
+ this.nodesCache = null;
1121
1341
  }
1122
1342
 
1343
+ /**
1344
+ * Recursively traverse expr.
1345
+ * If a value is a function, evaluate it.
1346
+ * If a value is an array, recurse on each item.
1347
+ * If it's a primitive, convert it to a Template.
1348
+ * Otherwise pass the item (which is now either a Template or a Node) to callback.
1349
+ * @param expr
1350
+ * @param callback {function(Node|Template)}
1351
+ *
1352
+ * TODO: have applyExactNodes() use this function. */
1353
+ exprToTemplates(expr, callback) {
1354
+ if (Array.isArray(expr))
1355
+ for (let subExpr of expr)
1356
+ this.exprToTemplates(subExpr, callback);
1357
+
1358
+ else if (typeof expr === 'function') {
1359
+ // TODO: One ExprPath can have multiple expr functions.
1360
+ // But if using it as a watch, it should only have one at the top level.
1361
+ // So maybe this is ok.
1362
+ Globals$1.currentExprPath = this; // Used by watch()
1363
+
1364
+ this.watchFunction = expr; // TODO: Only do this if it's a top level function.
1365
+ expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
1366
+ Globals$1.currentExprPath = null;
1367
+
1368
+ this.exprToTemplates(expr, callback);
1369
+ }
1370
+
1371
+ // String/Number/Date/Boolean
1372
+ else if (!(expr instanceof Template) && !(expr instanceof Node)){
1373
+ // Convert expression to a string.
1374
+ if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
1375
+ expr = '';
1376
+ else if (typeof expr !== 'string')
1377
+ expr += '';
1378
+
1379
+ // Get the same Template for the same string each time.
1380
+ // let template = Globals.stringTemplates[expr];
1381
+ // if (!template) {
1382
+ let template = new Template([expr], []);
1383
+ // Globals.stringTemplates[expr] = template;
1384
+ //}
1385
+
1386
+ // Recurse.
1387
+ this.exprToTemplates(template, callback);
1388
+ }
1389
+ else
1390
+ callback(expr);
1391
+ }
1123
1392
 
1124
1393
 
1125
1394
  /**
1126
- * Apply Nodes that are an exact match.
1395
+ * Try to apply Nodes that are an exact match, by finding existing nodes from the last render
1396
+ * that have the same value as created by the expr.
1397
+ * This is called from ExprPath.applyNodes().
1398
+ *
1127
1399
  * @param expr {Template|Node|Array|function|*}
1128
- * @param newNodes {(Node|Template)[]}
1129
- * @param secondPass {Array} Locations within newNodes to evaluate later. */
1130
- applyExact(expr, newNodes, secondPass) {
1400
+ * @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
1401
+ * @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
1402
+ * when it tries to find partial matches. */
1403
+ applyExactNodes(expr, newNodes, secondPass) {
1131
1404
 
1132
1405
  if (expr instanceof Template) {
1133
-
1134
1406
  let ng = this.getNodeGroup(expr, true);
1135
1407
  if (ng) {
1136
1408
 
@@ -1148,7 +1420,7 @@ class ExprPath {
1148
1420
  }
1149
1421
  }
1150
1422
 
1151
- // Node created by an expression.
1423
+ // Node(s) created by an expression.
1152
1424
  else if (expr instanceof Node) {
1153
1425
 
1154
1426
  // DocumentFragment created by an expression.
@@ -1161,45 +1433,50 @@ class ExprPath {
1161
1433
  // Arrays and functions.
1162
1434
  // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
1163
1435
  // but that consistently made the js-framework-benchmarks a few percentage points slower.
1164
- else if (Array.isArray(expr))
1436
+ else {
1437
+ this.exprToTemplates(expr, template => {
1438
+ this.applyExactNodes(template, newNodes, secondPass);
1439
+ });
1440
+
1441
+ }
1442
+
1443
+ // Old version
1444
+ /*else if (Array.isArray(expr))
1165
1445
  for (let subExpr of expr)
1166
- this.applyExact(subExpr, newNodes, secondPass);
1446
+ this.applyExactNodes(subExpr, newNodes, secondPass);
1167
1447
 
1168
1448
  else if (typeof expr === 'function') {
1169
- Globals.currentExprPath = [this, expr]; // Used by watch3()
1170
- let result = expr();
1449
+ // TODO: One ExprPath can have multiple expr functions.
1450
+ // But if using it as a watch, it should only have one at the top level.
1451
+ // So maybe this is ok.
1452
+ Globals.currentExprPath = this; // Used by watch()
1453
+
1454
+ this.watchFunction = expr; // TODO: Only do this if it's a top level function.
1455
+ let result = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
1171
1456
  Globals.currentExprPath = null;
1172
1457
 
1173
- this.applyExact(result, newNodes, secondPass);
1458
+ this.applyExactNodes(result, newNodes, secondPass);
1174
1459
  }
1175
1460
 
1176
- // Text
1461
+ // String
1177
1462
  else {
1178
- // Convert falsy values (but not 0) to empty string.
1179
- // Convert numbers to string so they compare the same.
1180
- let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
1181
-
1182
- // Fast path for updating the text of a single text node.
1183
- let first = this.nodeBefore.nextSibling;
1184
- if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
1185
- if (first.textContent !== text)
1186
- first.textContent = text;
1187
-
1188
- newNodes.push(first);
1463
+ // Convert expression to a string.
1464
+ let stringExpr = expr;
1465
+ if (expr === undefined || expr === false || expr === null) // Util.isFalsy()
1466
+ stringExpr = '';
1467
+ else if (typeof expr !== 'string')
1468
+ stringExpr = expr + '';
1469
+
1470
+ // Get the same Template for the same string each time.
1471
+ let template = Globals.stringTemplates[stringExpr];
1472
+ if (!template) {
1473
+ template = new Template([stringExpr], []);
1474
+ Globals.stringTemplates[stringExpr] = template;
1189
1475
  }
1190
1476
 
1191
- else {
1192
- // TODO: Optimize this into a Set or Map or something?
1193
- if (!this.existingTextNodes)
1194
- this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
1195
-
1196
- let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
1197
- if (idx !== -1)
1198
- newNodes.push(...this.existingTextNodes.splice(idx, 1));
1199
- else
1200
- newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
1201
- }
1202
- }
1477
+ // Recurse.
1478
+ this.applyExactNodes(template, newNodes, secondPass);
1479
+ }*/
1203
1480
  }
1204
1481
 
1205
1482
  applyMultipleAttribs(node, expr) {
@@ -1212,6 +1489,13 @@ class ExprPath {
1212
1489
  let oldNames = this.attrNames;
1213
1490
  this.attrNames = new Set();
1214
1491
  if (expr) {
1492
+ if (typeof expr === 'function') {
1493
+ Globals$1.currentExprPath = this; // Used by watch()
1494
+ this.watchFunction = expr; // used by renderWatched()
1495
+ expr = expr();
1496
+ Globals$1.currentExprPath = null;
1497
+ }
1498
+
1215
1499
  let attrs = (expr +'') // Split string into multiple attributes.
1216
1500
  .split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
1217
1501
  .map(text => text.trim())
@@ -1245,43 +1529,54 @@ class ExprPath {
1245
1529
 
1246
1530
  let eventName = this.attrName.slice(2); // remove "on-" prefix.
1247
1531
  let func;
1248
-
1249
- // Convert array to function.
1250
1532
  let args = [];
1251
- if (Array.isArray(expr)) {
1252
-
1253
- // oninput=${[this.doSomething, 'meow']}
1254
- if (typeof expr[0] === 'function') {
1255
- func = expr[0];
1256
- args = expr.slice(1);
1257
- }
1258
1533
 
1259
- // Undocumented.
1260
- // oninput=${[this, 'value']}
1261
- else {
1262
- func = setValue;
1263
- args = [expr[0], expr.slice(1), node];
1264
- node.value = delve(expr[0], expr.slice(1));
1265
- // root.render(); // TODO: This causes infinite recursion.
1266
- }
1534
+ // Convert array to function.
1535
+ // oninput=${[this.doSomething, 'meow']}
1536
+ if (Array.isArray(expr) && typeof expr[0] === 'function') {
1537
+ func = expr[0];
1538
+ args = expr.slice(1);
1267
1539
  }
1268
- else
1540
+ else if (typeof expr === 'function')
1269
1541
  func = expr;
1542
+ else
1543
+ throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
1544
+
1545
+ this.bindEvent(node, root, eventName, eventName, func, args);
1546
+ }
1270
1547
 
1271
- let nodeEvents = Globals.nodeEvents.get(node);
1548
+
1549
+ /**
1550
+ * Call function when eventName is triggerd on node.
1551
+ * @param node {HTMLElement}
1552
+ * @param root {HTMLElement}
1553
+ * @param key {string}
1554
+ * @param eventName {string}
1555
+ * @param func {function}
1556
+ * @param args {array}
1557
+ * @param capture {boolean} */
1558
+ bindEvent(node, root, key, eventName, func, args, capture=false) {
1559
+ let nodeEvents = Globals$1.nodeEvents.get(node);
1272
1560
  if (!nodeEvents) {
1273
- nodeEvents = {[eventName]: new Array(3)};
1274
- Globals.nodeEvents.set(node, nodeEvents);
1561
+ nodeEvents = {[key]: new Array(3)};
1562
+ Globals$1.nodeEvents.set(node, nodeEvents);
1275
1563
  }
1276
- let nodeEvent = nodeEvents[eventName];
1277
-
1564
+ let nodeEvent = nodeEvents[key];
1565
+ if (!nodeEvent)
1566
+ nodeEvents[key] = nodeEvent = new Array(3);
1278
1567
 
1568
+ if (typeof func !== 'function')
1569
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
1279
1570
 
1280
1571
  // If function has changed, remove and rebind the event.
1281
1572
  if (nodeEvent[0] !== func) {
1573
+
1574
+ // TODO: We should be removing event listeners when calling getNodeGroup(),
1575
+ // when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
1576
+ // instead of only when we rebind an event.
1282
1577
  let [existing, existingBound, _] = nodeEvent;
1283
1578
  if (existing)
1284
- node.removeEventListener(eventName, existingBound);
1579
+ node.removeEventListener(eventName, existingBound, capture);
1285
1580
 
1286
1581
  let originalFunc = func;
1287
1582
 
@@ -1297,74 +1592,155 @@ class ExprPath {
1297
1592
  nodeEvent[0] = originalFunc;
1298
1593
  nodeEvent[1] = boundFunc;
1299
1594
 
1300
- node.addEventListener(eventName, boundFunc);
1595
+ node.addEventListener(eventName, boundFunc, capture);
1301
1596
 
1302
1597
  // TODO: classic event attribs?
1303
- //el[attr.name] = e => // e.g. el.onclick = ...
1304
- // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el) // put "event", "el", and "this" in scope for the event code.
1598
+ //el[attr.name] = e => // e.g. el.onclick = ... // put "event", "el", and "this" in scope for the event code.
1599
+ // (new Function('event', 'el', attr.value)).bind(this.manager.rootEl)(e, el)
1305
1600
  }
1306
1601
 
1307
1602
  // Otherwise just update the args to the function.
1308
- nodeEvents[eventName][2] = args;
1603
+ nodeEvents[key][2] = args;
1309
1604
  }
1310
1605
 
1311
- applyValueAttrib(node, exprs, exprIndex) {
1312
- let expr = exprs[exprIndex];
1313
-
1314
- // Values to toggle an attribute
1315
- if (!this.attrValue && (expr === false || expr === null || expr === undefined))
1316
- node.removeAttribute(this.attrName);
1317
-
1318
- else if (!this.attrValue && expr === true)
1319
- node.setAttribute(this.attrName, '');
1606
+ /**
1607
+ * Handle values, including two-way binding.
1608
+ * @param node
1609
+ * @param exprs */
1610
+ // TODO: node is always this.nodeMarker?
1611
+ applyValueAttrib(node, exprs) {
1612
+ let expr = exprs[0];
1320
1613
 
1614
+ // Two-way binding between attributes
1321
1615
  // Passing a path to the value attribute.
1616
+ // Copies the attribute to the property when the input event fires.
1617
+ // value=${[this, 'value]'}
1618
+ // checked=${[this, 'isAgree']}
1322
1619
  // This same logic is in NodeGroup.createNewComponent() for components.
1323
- else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
1620
+ if (Util.isPath(expr)) {
1324
1621
  let [obj, path] = [expr[0], expr.slice(1)];
1325
- node.value = delve(obj, path);
1326
- node.addEventListener('input', () => {
1327
- delve(obj, path, Util.getInputValue(node));
1328
- }, true); // We use capture so we update the values before other events added by the user.
1622
+
1623
+ if (!obj)
1624
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
1625
+
1626
+ let value = delve(obj, path);
1627
+
1628
+ // Special case to allow setting select-multiple value from an array
1629
+ if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
1630
+ // Set the .selected property on the options having a value within value.
1631
+ let strValues = value.map(v => v + '');
1632
+ for (let option of node.options)
1633
+ option.selected = strValues.includes(option.value);
1634
+ }
1635
+ else {
1636
+ // TODO: should we remove isFalsy, since these are always props?
1637
+ let strValue = Util.isFalsy(value) ? '' : value;
1638
+
1639
+ // If we don't have this condition, when we call render(), the browser will scroll to the currently
1640
+ // selected item in a <select> and mess up manually scrolling to a different value.
1641
+ if (strValue !== node[this.attrName])
1642
+ node[this.attrName] = strValue;
1643
+ }
1644
+
1645
+ // TODO: We need to remove any old listeners, like in bindEventAttribute.
1646
+ // Does bindEvent() now handle that?
1647
+ let func = () => {
1648
+ let value = (this.attrName === 'value')
1649
+ ? Util.getInputValue(node)
1650
+ : node[this.attrName];
1651
+ delve(obj, path, value);
1652
+ };
1653
+
1654
+ // We use capture so we update the values before other events added by the user.
1655
+ // TODO: Bind to scroll events also?
1656
+ // What about resize events and width/height?
1657
+ this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
1329
1658
  }
1330
1659
 
1331
1660
  // Regular attribute
1332
1661
  else {
1333
- let value = [];
1334
-
1335
- // We go backward because NodeGroup.applyExprs() calls this function, and it goes backward through the exprs.
1336
- if (this.attrValue) {
1337
- for (let i=this.attrValue.length-1; i>=0; i--) {
1338
- value.unshift(this.attrValue[i]);
1339
- if (i > 0) {
1340
- let val = exprs[exprIndex];
1341
- if (val !== false && val !== null && val !== undefined)
1342
- value.unshift(val);
1343
- exprIndex--;
1662
+ // TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
1663
+ // Or make it a new PathType.
1664
+ //if (this.attrName === 'disabled')
1665
+ // debugger;
1666
+
1667
+ // hasOwnProperty() checks only the object, not the parents
1668
+ // this.attrName in node checks the node and the parents.
1669
+ // This version checks the html element it extends from, to see if has a setter set:
1670
+ // Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
1671
+ //let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
1672
+ let isProp = this.isHtmlProperty;
1673
+ if (isProp === undefined)
1674
+ isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
1675
+
1676
+ // Values to toggle an attribute
1677
+ let multiple = this.attrValue;
1678
+ if (!multiple) {
1679
+ Globals$1.currentExprPath = this; // Used by watch()
1680
+ if (typeof expr === 'function') {
1681
+ if (this.type === 4) { // Don't evaluate functions before passing them to components
1682
+ return
1344
1683
  }
1684
+ this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
1685
+ expr = expr();
1345
1686
  }
1346
- exprIndex ++;
1687
+ else
1688
+ expr = Util.makePrimitive(expr);
1689
+ Globals$1.currentExprPath = null;
1690
+ }
1691
+ if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
1692
+ if (isProp)
1693
+ node[this.attrName] = false;
1694
+ node.removeAttribute(this.attrName);
1695
+ }
1696
+ else if (!multiple && expr === true) {
1697
+ if (isProp)
1698
+ node[this.attrName] = true;
1699
+ node.setAttribute(this.attrName, '');
1347
1700
  }
1348
- else
1349
- value.unshift(expr);
1350
1701
 
1351
- let joinedValue = value.join('');
1702
+ // A non-toggled attribute
1703
+ else {
1352
1704
 
1353
- // Only update attributes if the value has changed.
1354
- // The .value property is special. If it changes we don't update the attribute.
1355
- let oldVal = this.attrName === 'value' ? node.value : node.getAttribute(this.attrName);
1356
- if (oldVal !== joinedValue) {
1357
- node.setAttribute(this.attrName, joinedValue);
1358
- }
1705
+ // If it's a series of expressions among strings, join them together.
1706
+ let joinedValue;
1707
+ if (multiple) {
1708
+ let value = [];
1709
+ for (let i = 0; i < this.attrValue.length; i++) {
1710
+ value.push(this.attrValue[i]);
1711
+ if (i < this.attrValue.length - 1) {
1712
+ Globals$1.currentExprPath = this; // Used by watch()
1713
+ let val = Util.makePrimitive(exprs[i]);
1714
+ Globals$1.currentExprPath = null;
1715
+ if (!Util.isFalsy(val))
1716
+ value.push(val);
1717
+ }
1718
+ }
1719
+ joinedValue = value.join('');
1720
+ }
1359
1721
 
1360
- // This is needed for setting input.value, .checked, option.selected, etc.
1361
- // But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
1362
- // TODO: How to tell which is which?
1363
- if (this.attrName in node)
1364
- node[this.attrName] = joinedValue;
1722
+ // If the attribute is one expression with no strings:
1723
+ else
1724
+ joinedValue = expr;
1725
+
1726
+ // Only update attributes if the value has changed.
1727
+ // This is needed for setting input.value, .checked, option.selected, etc.
1728
+
1729
+ let oldVal = isProp
1730
+ ? node[this.attrName]
1731
+ : node.getAttribute(this.attrName);
1732
+ if (oldVal !== joinedValue) {
1733
+
1734
+ // <textarea value=${expr}></textarea>
1735
+ // Without this branch we have no way to set the value of a textarea,
1736
+ // since we also prohibit expressions that are a child of textarea.
1737
+ if (isProp)
1738
+ node[this.attrName] = joinedValue;
1739
+ // TODO: Putting an 'else' here would be more performant
1740
+ node.setAttribute(this.attrName, joinedValue);
1741
+ }
1742
+ }
1365
1743
  }
1366
-
1367
- return exprIndex;
1368
1744
  }
1369
1745
 
1370
1746
 
@@ -1380,7 +1756,8 @@ class ExprPath {
1380
1756
  let nodeMarker, nodeBefore;
1381
1757
  let root = newRoot;
1382
1758
  let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
1383
- for (let i=path.length-1; i>0; i--) // Resolve the path.
1759
+ let length = path.length-1;
1760
+ for (let i=length; i>0; i--) // Resolve the path.
1384
1761
  root = root.childNodes[path[i]];
1385
1762
  let childNodes = root.childNodes;
1386
1763
 
@@ -1420,7 +1797,7 @@ class ExprPath {
1420
1797
 
1421
1798
  /**
1422
1799
  * Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
1423
- * @returns {boolean} Returns false if Nodes werne't removed, and they should instead be removed manually. */
1800
+ * @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
1424
1801
  fastClear() {
1425
1802
  let parent = this.nodeBefore.parentNode;
1426
1803
  if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
@@ -1456,6 +1833,10 @@ class ExprPath {
1456
1833
  // result2.push(...ng.getNodes())
1457
1834
  // return result2;
1458
1835
 
1836
+ if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
1837
+ return [this.nodeMarker];
1838
+ }
1839
+
1459
1840
 
1460
1841
  let result;
1461
1842
 
@@ -1480,7 +1861,8 @@ class ExprPath {
1480
1861
  return result;
1481
1862
  }
1482
1863
 
1483
- getParentNode() { // Same as this.parentNode
1864
+ /** @return {HTMLElement|ParentNode} */
1865
+ getParentNode() {
1484
1866
  return this.nodeMarker.parentNode
1485
1867
  }
1486
1868
 
@@ -1497,27 +1879,39 @@ class ExprPath {
1497
1879
  * or createa new NodeGroup from the template.
1498
1880
  * @return {NodeGroup} */
1499
1881
  getNodeGroup(template, exact=true) {
1500
- //if (exact && this.nodeGroupsFree.isEmpty())
1501
- // return null;
1502
1882
 
1503
1883
  let result;
1884
+ let collection = this.nodeGroupsAttachedAvailable;
1885
+
1886
+ // TODO: Would it be faster to maintain a separate list of detached nodegroups?
1887
+ if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
1888
+ result = collection.deleteAny(template.getExactKey());
1889
+ if (!result) { // try searching detached
1890
+ collection = this.nodeGroupsDetachedAvailable;
1891
+ result = collection.deleteAny(template.getExactKey());
1892
+ }
1504
1893
 
1505
- if (exact) {
1506
- result = this.nodeGroupsFree.delete(template.getExactKey());
1507
1894
  if (result) // also delete the matching close key.
1508
- this.nodeGroupsFree.delete(template.getCloseKey(), result);
1509
- else
1895
+ collection.deleteSpecific(template.getCloseKey(), result);
1896
+ else {
1510
1897
  return null;
1898
+ }
1511
1899
  }
1512
1900
 
1513
1901
  // Find a close match.
1514
1902
  // This is a match that has matching html, but different expressions applied.
1515
1903
  // We can then apply the expressions to make it an exact match.
1516
- else {
1517
- result = this.nodeGroupsFree.delete(template.getCloseKey());
1904
+ // If the template has no expressions, the key is the html, and we've already searched for an exact match. There won't be an inexact match.
1905
+ else if (template.exprs.length) {
1906
+ result = collection.deleteAny(template.getCloseKey());
1907
+ if (!result) { // try searching detached
1908
+ collection = this.nodeGroupsDetachedAvailable;
1909
+ result = collection.deleteAny(template.getCloseKey());
1910
+ }
1911
+
1518
1912
  if (result) {
1519
1913
 
1520
- this.nodeGroupsFree.delete(result.exactKey, result);
1914
+ collection.deleteSpecific(result.exactKey, result);
1521
1915
 
1522
1916
  // Update this close match with the new expression values.
1523
1917
  result.applyExprs(template.exprs);
@@ -1529,89 +1923,85 @@ class ExprPath {
1529
1923
  result = new NodeGroup(template, this);
1530
1924
 
1531
1925
  // old:
1532
- this.nodeGroupsInUse.push(result);
1533
-
1534
- // new:
1535
- // let ngiu = this.nodeGroupsInUse;
1536
- // ngiu.add(result.exactKey, result);
1537
- // ngiu.add(result.closeKey, result);
1926
+ this.nodeGroupsRendered.push(result);
1538
1927
 
1539
1928
 
1540
1929
  return result;
1541
1930
  }
1542
1931
 
1932
+ isComponent() {
1933
+ // Events won't have type===Component.
1934
+ // TODO: Have a special flag for components instead of it being on the type?
1935
+ return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
1936
+ }
1543
1937
 
1544
1938
  /**
1939
+ * TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
1940
+ * Nodes that have been used during the current render().
1545
1941
  * Used with getNodeGroup() and freeNodeGroups().
1546
1942
  * TODO: Use an array of WeakRef so the gc can collect them?
1547
1943
  * TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
1548
1944
  * @type {NodeGroup[]} */
1549
- nodeGroupsInUse = [];
1550
-
1551
- /** @type {MultiValueMap<key:string, value:NodeGroup>} */
1552
- //nodeGroupsInUse = new MultiValueMap();
1945
+ nodeGroupsRendered = [];
1553
1946
 
1554
1947
  /**
1948
+ * Nodes that were added to the web component during the last render(), but are available to be used again.
1555
1949
  * Used with getNodeGroup() and freeNodeGroups().
1556
1950
  * Each NodeGroup is here twice, once under an exact key, and once under the close key.
1557
1951
  * @type {MultiValueMap<key:string, value:NodeGroup>} */
1558
- nodeGroupsFree = new MultiValueMap();
1952
+ nodeGroupsAttachedAvailable = new MultiValueMap();
1953
+
1954
+ /**
1955
+ * Nodes that were not added to the web component during the last render(), and available to be used again.
1956
+ * @type {MultiValueMap} */
1957
+ nodeGroupsDetachedAvailable = new MultiValueMap();
1559
1958
 
1560
1959
 
1561
1960
  /**
1562
- * Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
1961
+ * Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
1962
+ * Called at the beginning of applyNodes() so it can have NodeGroups to use.
1563
1963
  * TODO: this could run as needed in getNodeGroup? */
1564
1964
  freeNodeGroups() {
1565
- // old:
1566
- let ngf = this.nodeGroupsFree;
1567
- for (let ng of this.nodeGroupsInUse) {
1568
- ngf.add(ng.exactKey, ng);
1569
- ngf.add(ng.closeKey, ng);
1965
+ // Add nodes that weren't used during render() to nodeGroupsDetached
1966
+ let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
1967
+ let detached = this.nodeGroupsDetachedAvailable.data;
1968
+ for (let key in previouslyAttached) {
1969
+ let set = detached[key];
1970
+ if (!set)
1971
+ detached[key] = previouslyAttached[key];
1972
+ else
1973
+ for (let ng of previouslyAttached[key])
1974
+ set.add(ng);
1975
+ }
1976
+
1977
+ // Add nodes that were used during render() to nodeGroupsRendered.
1978
+ this.nodeGroupsAttachedAvailable = new MultiValueMap();
1979
+ let nga = this.nodeGroupsAttachedAvailable;
1980
+ for (let ng of this.nodeGroupsRendered) {
1981
+ nga.add(ng.exactKey, ng);
1982
+ nga.add(ng.closeKey, ng);
1570
1983
  }
1571
- this.nodeGroupsInUse = [];
1572
1984
 
1573
- // new:
1574
- // for (let key in this.nodeGroupsFree.data)
1575
- // for (let item of this.nodeGroupsFree.data[key])
1576
- // this.nodeGroupsInUse.add(key, item);
1577
- //
1578
- // this.nodeGroupsFree = this.nodeGroupsInUse;
1579
- // this.nodeGroupsInUse = new MultiValueMap();
1985
+ this.nodeGroupsRendered = [];
1580
1986
  }
1581
1987
 
1582
1988
 
1583
1989
  }
1584
1990
 
1585
-
1586
-
1587
- /**
1588
- *
1589
- * @param root
1590
- * @param path {string[]}
1591
- * @param node {HTMLElement}
1592
- */
1593
- function setValue(root, path, node) {
1594
- let val = node.value;
1595
- if (node.type === 'number')
1596
- val = parseFloat(val);
1597
-
1598
- delve(root, path, val);
1599
- }
1600
-
1601
1991
  /** @enum {int} */
1602
- const PathType = {
1992
+ const ExprPathType = {
1603
1993
  /** Child of a node */
1604
1994
  Content: 1,
1605
-
1995
+
1606
1996
  /** One or more whole attributes */
1607
- Multiple: 2,
1608
-
1997
+ AttribMultiple: 2,
1998
+
1609
1999
  /** Value of an attribute. */
1610
- Value: 3,
1611
-
2000
+ AttribValue: 3,
2001
+
1612
2002
  /** Value of an attribute being passed to a component. */
1613
- Component: 4,
1614
-
2003
+ ComponentAttribValue: 4,
2004
+
1615
2005
  /** Expressions inside Html comments. */
1616
2006
  Comment: 5,
1617
2007
 
@@ -1637,116 +2027,177 @@ function getNodePath(node) {
1637
2027
  * Note that the path is backward, with the outermost element at the end.
1638
2028
  * @param root {HTMLElement|Document|DocumentFragment|ParentNode}
1639
2029
  * @param path {int[]}
1640
- * @returns {Node|HTMLElement} */
2030
+ * @returns {Node|HTMLElement|HTMLStyleElement} */
1641
2031
  function resolveNodePath(root, path) {
1642
2032
  for (let i=path.length-1; i>=0; i--)
1643
2033
  root = root.childNodes[path[i]];
1644
2034
  return root;
1645
2035
  }
1646
2036
 
2037
+ class HtmlParser {
2038
+ constructor() {
2039
+ this.defaultState = {
2040
+ context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
2041
+ quote: null, // possible values: null, '"', "'"
2042
+ buffer: '',
2043
+ lastChar: null
2044
+ };
2045
+ this.state = {...this.defaultState};
2046
+ }
2047
+
2048
+ reset() {
2049
+ this.state = {...this.defaultState};
2050
+ return this.state.context;
2051
+ }
2052
+
2053
+ /**
2054
+ * Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
2055
+ * @param html {string}
2056
+ * @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
2057
+ * Called every time the context changes, and again at the last context.
2058
+ * @return {('Attribute','Text','Tag')} The context at the end of html. */
2059
+ parse(html, onContextChange=null) {
2060
+ if (html === null)
2061
+ return this.reset();
2062
+
2063
+ for (let i = 0; i < html.length; i++) {
2064
+ const char = html[i];
2065
+ switch (this.state.context) {
2066
+ case HtmlParser.Text:
2067
+ if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
2068
+ onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
2069
+ this.state.context = HtmlParser.Tag;
2070
+ this.state.buffer = '';
2071
+ }
2072
+ break;
2073
+ case HtmlParser.Tag:
2074
+ if (char === '>') {
2075
+ onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
2076
+ this.state.context = HtmlParser.Text;
2077
+ this.state.quote = null;
2078
+ this.state.buffer = '';
2079
+ }
2080
+ else if (char === ' ' && !this.state.buffer) {
2081
+ // No attribute name is present. Skipping the space.
2082
+ continue;
2083
+ }
2084
+ else if (char === ' ' || char === '/' || char === '?') {
2085
+ this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
2086
+ }
2087
+ else if (char === '"' || char === "'" || char === '=') {
2088
+ onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
2089
+ this.state.context = HtmlParser.Attribute;
2090
+ this.state.quote = char === '=' ? null : char;
2091
+ this.state.buffer = '';
2092
+ }
2093
+ else
2094
+ this.state.buffer += char;
2095
+ break;
2096
+ case HtmlParser.Attribute:
2097
+ // Start an attribute quote.
2098
+ if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
2099
+ this.state.quote = char;
2100
+ }
2101
+ else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
2102
+ onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
2103
+ this.state.context = HtmlParser.Tag;
2104
+ this.state.quote = null;
2105
+ this.state.buffer = '';
2106
+ }
2107
+ else if (!this.state.quote && char === '>') {
2108
+ onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
2109
+ this.state.context = HtmlParser.Text;
2110
+ this.state.quote = null;
2111
+ this.state.buffer = '';
2112
+ }
2113
+ else if (char !== ' ')
2114
+ this.state.buffer += char;
2115
+
2116
+ break;
2117
+ }
2118
+ }
2119
+ onContextChange?.(html, html.length, this.state.context, null);
2120
+ return this.state.context;
2121
+ }
2122
+ }
2123
+
2124
+ HtmlParser.Attribute = 'Attribute';
2125
+ HtmlParser.Text = 'Text';
2126
+ HtmlParser.Tag = 'Tag';
2127
+
1647
2128
  /**
1648
2129
  * A Shell is created from a tagged template expression instantiated as Nodes,
1649
2130
  * but without any expressions filled in.
1650
2131
  * Only one Shell is created for all the items in a loop.
1651
2132
  *
1652
2133
  * When a NodeGroup is created from a Template's html strings,
1653
- * the NodeGroup then clones the Shell's fragmentn to be its nodes. */
2134
+ * the NodeGroup then clones the Shell's fragment to be its nodes. */
1654
2135
  class Shell {
1655
2136
 
1656
2137
  /**
1657
- * @type {DocumentFragment} DOM parent of the shell nodes. */
2138
+ * @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
1658
2139
  fragment;
1659
2140
 
1660
2141
  /** @type {ExprPath[]} Paths to where expressions should go. */
1661
2142
  paths = [];
1662
2143
 
1663
- // Embeds and ids
1664
- events = [];
2144
+ // Elements with events. Not yet used.
2145
+ // events = [];
1665
2146
 
1666
2147
  /** @type {int[][]} Array of paths */
1667
2148
  ids = [];
2149
+
2150
+ /** @type {int[][]} Array of paths */
1668
2151
  scripts = [];
2152
+
2153
+ /** @type {int[][]} Array of paths */
1669
2154
  styles = [];
1670
2155
 
2156
+ /** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
1671
2157
  staticComponents = [];
1672
2158
 
2159
+ /** @type {{path:int[], attribs:Object<string, string>}[]} */
2160
+ //componentAttribs = [];
2161
+
1673
2162
 
1674
2163
 
1675
2164
  /**
1676
2165
  * Create the nodes but without filling in the expressions.
1677
2166
  * This is useful because the expression-less nodes created by a template can be cached.
1678
- * @param html {string[]} */
2167
+ * @param html {string[]} Html strings, split on places where an expression exists. */
1679
2168
  constructor(html=null) {
1680
2169
  if (!html)
1681
2170
  return;
1682
2171
 
1683
2172
 
1684
2173
 
1685
- // 1. Add placeholders
1686
- // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
1687
- let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
1688
-
1689
- let buffer = [];
1690
- let commentPlaceholder = `<!--!✨!-->`;
1691
- let componentNames = {};
1692
-
1693
- htmlContext(null); // Reset the context.
1694
- for (let i=0; i<html.length; i++) {
1695
- let lastHtml = html[i];
1696
- let context = htmlContext(lastHtml);
1697
-
1698
- // Swap out Embedded Solarite Components with ${} attributes.
1699
- // Later, NodeGroup.render() will search for these and replace them with the real components.
1700
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1701
- if (context === htmlContext.Attribute) {
1702
-
1703
- let lastIndex, lastMatch;
1704
- lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
1705
- lastIndex = index+1; // +1 for after opening <
1706
- lastMatch = match.slice(1);
1707
- });
1708
-
1709
- if (lastMatch) {
1710
- let newTagName = lastMatch + '-solarite-placeholder';
1711
- lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
1712
- componentNames[lastMatch] = newTagName;
1713
- }
1714
- }
1715
-
1716
- buffer.push(lastHtml);
1717
- //console.log(lastHtml, context)
1718
- if (i < html.length-1)
1719
- if (context === htmlContext.Text)
1720
- buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
1721
- else
1722
- buffer.push(String.fromCharCode(placeholder+i));
2174
+ if (html.length === 1 && !html[0].match(/[<&]/)) {
2175
+ this.fragment = document.createTextNode(html[0]);
2176
+ return;
1723
2177
  }
1724
2178
 
1725
- // 2. Create elements from html with placeholders.
1726
- let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
1727
- let joinedHtml = buffer.join('');
1728
2179
 
1729
- // Replace '-solarite-placeholder' close tags.
1730
- // TODO: is there a better way? What if the close tag is inside a comment?
1731
- for (let name in componentNames)
1732
- joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
1733
-
1734
- if (joinedHtml)
1735
- template.innerHTML = joinedHtml;
1736
- else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
1737
- template.content.append(document.createTextNode(''));
2180
+ // 1. Add placeholders
2181
+ let joinedHtml = Shell.addPlaceholders(html);
2182
+
2183
+ let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
2184
+ if (joinedHtml)
2185
+ template.innerHTML = joinedHtml;
2186
+ else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
2187
+ template.content.append(document.createTextNode(''));
1738
2188
  this.fragment = template.content;
1739
2189
 
1740
- // 3. Find placeholders
2190
+ // 2. Find placeholders
1741
2191
  let node;
1742
2192
  let toRemove = [];
1743
- const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
2193
+ let placeholdersUsed = 0;
2194
+ const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
1744
2195
  while (node = walker.nextNode()) {
1745
2196
 
1746
2197
  // Remove previous after each iteration, so paths will still be calculated correctly.
1747
2198
  toRemove.map(el => el.remove());
1748
2199
  toRemove = [];
1749
-
2200
+
1750
2201
  // Replace attributes
1751
2202
  if (node.nodeType === 1) {
1752
2203
  for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
@@ -1754,7 +2205,8 @@ class Shell {
1754
2205
  // Whole attribute
1755
2206
  let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
1756
2207
  if (matches) {
1757
- this.paths.push(new ExprPath(null, node, PathType.Multiple));
2208
+ this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
2209
+ placeholdersUsed ++;
1758
2210
  node.removeAttribute(matches[0]);
1759
2211
  }
1760
2212
 
@@ -1763,16 +2215,17 @@ class Shell {
1763
2215
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
1764
2216
  if (parts.length > 1) {
1765
2217
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
1766
- let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
2218
+ let type = isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
1767
2219
 
1768
2220
  this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
2221
+ placeholdersUsed += parts.length - 1;
1769
2222
  node.setAttribute(attr.name, parts.join(''));
1770
2223
  }
1771
2224
  }
1772
2225
  }
1773
2226
  }
1774
2227
  // Replace comment placeholders
1775
- else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
2228
+ else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
1776
2229
 
1777
2230
  // Get or create nodeBefore.
1778
2231
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
@@ -1797,12 +2250,14 @@ class Shell {
1797
2250
  }
1798
2251
 
1799
2252
 
1800
-
1801
-
1802
- let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
1803
-
2253
+ let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
1804
2254
  this.paths.push(path);
2255
+ placeholdersUsed ++;
1805
2256
  }
2257
+
2258
+ else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
2259
+ throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
2260
+
1806
2261
 
1807
2262
 
1808
2263
  // Sometimes users will comment out a block of html code that has expressions.
@@ -1813,8 +2268,9 @@ class Shell {
1813
2268
  let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
1814
2269
  for (let i=0; i<parts.length-1; i++) {
1815
2270
  let path = new ExprPath(node.previousSibling, node);
1816
- path.type = PathType.Comment;
2271
+ path.type = ExprPathType.Comment;
1817
2272
  this.paths.push(path);
2273
+ placeholdersUsed ++;
1818
2274
  }
1819
2275
  }
1820
2276
 
@@ -1832,8 +2288,9 @@ class Shell {
1832
2288
  }
1833
2289
 
1834
2290
  for (let i=0, node; node=placeholders[i]; i++) {
1835
- let path = new ExprPath(node.previousSibling, node, PathType.Content);
2291
+ let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
1836
2292
  this.paths.push(path);
2293
+ placeholdersUsed ++;
1837
2294
 
1838
2295
 
1839
2296
  }
@@ -1845,17 +2302,17 @@ class Shell {
1845
2302
  }
1846
2303
  toRemove.map(el => el.remove());
1847
2304
 
2305
+ // Less than or equal because there can be one path to multiple expressions
2306
+ // if those expressions are in the same attribute value.
2307
+ if (placeholdersUsed !== html.length-1)
2308
+ throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
2309
+
1848
2310
  // Handle solarite-placeholder's.
1849
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
1850
- //if (componentNames.size)
1851
- // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
1852
2311
 
1853
- // Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
2312
+ // 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
1854
2313
  // that happens in NodeGroup.applyComponentExprs()
1855
- for (let el of this.fragment.querySelectorAll('[is]')) {
2314
+ for (let el of this.fragment.querySelectorAll('[is]'))
1856
2315
  el.setAttribute('_is', el.getAttribute('is'));
1857
- // this.components.push(el);
1858
- }
1859
2316
 
1860
2317
  for (let path of this.paths) {
1861
2318
  if (path.nodeBefore)
@@ -1863,16 +2320,63 @@ class Shell {
1863
2320
  path.nodeMarkerPath = getNodePath(path.nodeMarker);
1864
2321
 
1865
2322
  // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
1866
- if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
2323
+ if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
1867
2324
  (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
1868
- path.type = PathType.Component;
2325
+ path.type = ExprPathType.ComponentAttribValue;
1869
2326
  }
1870
2327
  }
1871
2328
 
1872
2329
  this.findEmbeds();
1873
2330
 
1874
2331
 
1875
- } // end constructor
2332
+ }
2333
+
2334
+ /**
2335
+ * 1. Add a Unicode placeholder char for where expressions go within attributes.
2336
+ * 2. Add a comment placeholder for where expressions are children of other nodes.
2337
+ * 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
2338
+ * @param htmlChunks {string[]}
2339
+ * @returns {string} */
2340
+ static addPlaceholders(htmlChunks) {
2341
+ let tokens = [];
2342
+
2343
+ function addToken(token, context) {
2344
+
2345
+ if (context === HtmlParser.Tag) {
2346
+ // Find Solarite Components tags and append -solarite-placeholder to their tag names.
2347
+ // This way we can gather their constructor arguments and their children before we call their constructor.
2348
+ // Later, NodeGroup.createNewComponent() will replace them with the real components.
2349
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2350
+ token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder');
2351
+ }
2352
+ tokens.push(token);
2353
+ }
2354
+
2355
+ let htmlParser = new HtmlParser(); // Reset the context.
2356
+ for (let i = 0; i < htmlChunks.length; i++) {
2357
+ let lastHtml = htmlChunks[i];
2358
+
2359
+ // Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
2360
+ let lastIndex = 0;
2361
+ let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
2362
+ if (lastIndex !== index) {
2363
+ let token = html.slice(lastIndex, index);
2364
+ addToken(token, oldContext);
2365
+ }
2366
+ lastIndex = index;
2367
+ });
2368
+
2369
+ // Insert placeholders
2370
+ if (i < htmlChunks.length - 1) {
2371
+ if (context === HtmlParser.Text)
2372
+ tokens.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
2373
+ else
2374
+ tokens.push(String.fromCharCode(attribPlaceholder + i));
2375
+ }
2376
+ }
2377
+
2378
+ return tokens.join('');
2379
+ }
1876
2380
 
1877
2381
  /**
1878
2382
  * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
@@ -1884,36 +2388,30 @@ class Shell {
1884
2388
  * this.staticComponents */
1885
2389
  findEmbeds() {
1886
2390
  this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
2391
+
2392
+ // TODO: only find styles that have ExprPaths in them?
1887
2393
  this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
1888
2394
 
1889
2395
  let idEls = this.fragment.querySelectorAll('[id],[data-id]');
1890
-
1891
2396
 
1892
2397
  // Check for valid id names.
1893
2398
  for (let el of idEls) {
1894
2399
  let id = el.getAttribute('data-id') || el.getAttribute('id');
1895
- if (div.hasOwnProperty(id))
2400
+ if (Globals$1.div.hasOwnProperty(id))
1896
2401
  throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
1897
2402
  }
1898
2403
 
1899
-
1900
2404
  this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
1901
2405
 
1902
- // Events (not yet used)
1903
2406
  for (let el of this.fragment.querySelectorAll('*')) {
1904
- for (let attrib of el.attributes)
1905
- if (isEvent(attrib.name))
1906
- this.events.push([attrib.name, getNodePath(el)]);
1907
-
1908
2407
  if (el.tagName.includes('-') || el.hasAttribute('_is'))
1909
2408
 
1910
- // Dynamic components have attributes with expression values.
2409
+ // Dynamic components are components that have attributes with expression values.
1911
2410
  // They are created from applyExprs()
1912
2411
  // But static components are created in a separate path inside the NodeGroup constructor.
1913
2412
  if (!this.paths.find(path => path.nodeMarker === el))
1914
2413
  this.staticComponents.push(getNodePath(el));
1915
2414
  }
1916
-
1917
2415
  }
1918
2416
 
1919
2417
  /**
@@ -1921,10 +2419,10 @@ class Shell {
1921
2419
  * @param htmlStrings {string[]} Typically comes from a Template.
1922
2420
  * @returns {Shell} */
1923
2421
  static get(htmlStrings) {
1924
- let result = Globals.shells.get(htmlStrings);
2422
+ let result = Globals$1.shells.get(htmlStrings);
1925
2423
  if (!result) {
1926
2424
  result = new Shell(htmlStrings);
1927
- Globals.shells.set(htmlStrings, result); // cache
2425
+ Globals$1.shells.set(htmlStrings, result); // cache
1928
2426
  }
1929
2427
 
1930
2428
 
@@ -1932,7 +2430,14 @@ class Shell {
1932
2430
  }
1933
2431
 
1934
2432
 
1935
- }
2433
+ }
2434
+
2435
+
2436
+ const commentPlaceholder = `<!--!✨!-->`;
2437
+
2438
+
2439
+ // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
2440
+ const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
1936
2441
 
1937
2442
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
1938
2443
 
@@ -1958,7 +2463,8 @@ class NodeGroup {
1958
2463
  startNode;
1959
2464
 
1960
2465
  /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
1961
- * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
2466
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
2467
+ * TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
1962
2468
  endNode;
1963
2469
 
1964
2470
  /** @type {ExprPath[]} */
@@ -1976,11 +2482,11 @@ class NodeGroup {
1976
2482
  nodesCache;
1977
2483
 
1978
2484
  /**
2485
+ * A map between <style> Elements and their text content.
2486
+ * This lets NodeGroup.updateStyles() see when the style text has changed.
1979
2487
  * @type {?Map<HTMLStyleElement, string>} */
1980
2488
  styles;
1981
2489
 
1982
- currentComponentProps = {};
1983
-
1984
2490
 
1985
2491
  /**
1986
2492
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
@@ -1988,14 +2494,26 @@ class NodeGroup {
1988
2494
  * @param parentPath {?ExprPath} */
1989
2495
  constructor(template, parentPath=null) {
1990
2496
  if (!(this instanceof RootNodeGroup)) {
2497
+
1991
2498
  let [fragment, shell] = this.init(template, parentPath);
1992
2499
 
1993
- this.updatePaths(fragment, shell.paths);
2500
+ if (fragment && template.exprs.length) {
2501
+ this.updatePaths(fragment, shell.paths);
1994
2502
 
1995
- this.activateEmbeds(fragment, shell);
2503
+ // Static web components can sometimes have children created via expressions.
2504
+ // But calling applyExprs() will mess up the shell's path to them.
2505
+ // So we find them first, then call activateStaticComponents() after their children have been created.
2506
+ let staticComponents = this.findStaticComponents(fragment, shell);
1996
2507
 
1997
- // Apply exprs
1998
- this.applyExprs(template.exprs);
2508
+ this.activateEmbeds(fragment, shell);
2509
+
2510
+ // Apply exprs
2511
+ this.applyExprs(template.exprs);
2512
+
2513
+ this.activateStaticComponents(staticComponents);
2514
+ }
2515
+ else if (shell)
2516
+ this.activateEmbeds(fragment, shell);
1999
2517
  }
2000
2518
  }
2001
2519
 
@@ -2023,57 +2541,103 @@ class NodeGroup {
2023
2541
  template.nodeGroup = this;
2024
2542
 
2025
2543
  // Get a cached version of the parsed and instantiated html, and ExprPaths.
2026
- let shell = Shell.get(template.html);
2027
- let fragment = shell.fragment.cloneNode(true);
2028
2544
 
2029
- let childNodes = fragment.childNodes;
2030
- this.startNode = childNodes[0];
2031
- this.endNode = childNodes[childNodes.length - 1];
2545
+ // If it's just a text node, skip a bunch of unnecessary steps.
2546
+ if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
2547
+ //let doc = this.rootNg.startNode?.ownerDocument || document;
2548
+ let textNode = document.createTextNode(template.html[0]);
2549
+
2550
+ this.startNode = this.endNode = textNode;
2551
+ return [];
2552
+ }
2553
+ else {
2554
+ let shell = Shell.get(template.html);
2555
+ let fragment = shell.fragment.cloneNode(true);
2032
2556
 
2033
- return [fragment, shell];
2557
+ if (fragment instanceof DocumentFragment) {
2558
+ let childNodes = fragment.childNodes;
2559
+ this.startNode = childNodes[0];
2560
+ this.endNode = childNodes[childNodes.length - 1];
2561
+ }
2562
+ else {
2563
+ this.startNode = this.endNode = fragment;
2564
+ }
2565
+ return [fragment, shell];
2566
+ }
2034
2567
  }
2035
2568
 
2036
2569
  /**
2037
2570
  * Use the paths to insert the given expressions.
2038
2571
  * Dispatches expression handling to other functions depending on the path type.
2039
2572
  * @param exprs {(*|*[]|function|Template)[]}
2040
- * @param paths {?ExprPath[]} Optional. */
2573
+ * @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
2041
2574
  applyExprs(exprs, paths=null) {
2042
2575
  paths = paths || this.paths;
2043
2576
 
2044
2577
 
2045
2578
 
2046
- // Update exprs at paths.
2047
- let exprIndex = exprs.length-1, expr, lastNode;
2579
+ // Things to consider:
2580
+ // 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
2581
+ // 2. One component may need to use multiple attribute paths to be instantiated.
2582
+ // 3. We apply them in reverse order so that a <select> box has its children created from an expression
2583
+ // before its instantiated and its value attribute is set via an expression.
2584
+
2585
+ let exprIndex = exprs.length - 1; // Update exprs at paths.
2586
+ let lastComponentPathIndex;
2587
+ 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.
2588
+ for (let i = paths.length - 1, path; path = paths[i]; i--) {
2589
+ let prevPath = paths[i - 1];
2590
+ let nextPath = paths[i + 1];
2591
+
2592
+ // Get the expressions associated with this path.
2593
+ if (path.attrValue?.length > 2) {
2594
+ let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
2595
+ pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
2596
+ exprIndex -= pathExprs[i].length;
2597
+ } else {
2598
+ pathExprs[i] = [exprs[exprIndex]];
2599
+ exprIndex--;
2600
+ }
2048
2601
 
2049
- // We apply them in reverse order so that a <select> box has its options created from an expression
2050
- // before its value attribute is set via an expression.
2051
- for (let path of paths.toReversed()) {
2052
- expr = exprs[exprIndex];
2602
+ // TODO: Need to end and restart this block when going from one component to the next?
2603
+ // Think of having two adjacent components.
2604
+ // But the dynamicAttribsAdjacet test already passes.
2053
2605
 
2054
- // Nodes
2606
+ // If a component:
2607
+ // 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
2608
+ // 2. Otherwise send them to its render function.
2609
+ // Components with no expressions as attributes are instead activated in activateEmbeds().
2610
+ if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
2055
2611
 
2056
- // This is necessary both here and below.
2057
- if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
2058
- this.applyComponentExprs(lastNode, this.currentComponentProps);
2059
- this.currentComponentProps = {};
2060
- }
2612
+ if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
2613
+ lastComponentPathIndex = i;
2614
+ let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
2061
2615
 
2062
- exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
2616
+ if (isFirstComponentPath) {
2063
2617
 
2064
- lastNode = path.nodeMarker;
2618
+ let componentProps = {};
2619
+ for (let j=i; j<=lastComponentPathIndex; j++) {
2620
+ let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
2621
+ componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
2622
+ }
2065
2623
 
2624
+ this.applyComponentExprs(path.nodeMarker, componentProps);
2066
2625
 
2067
- exprIndex--;
2068
- } // end for(path of this.paths)
2626
+ // Set attributes on component.
2627
+ for (let j=i; j<=lastComponentPathIndex; j++)
2628
+ paths[j].apply(pathExprs[j]);
2629
+ }
2630
+ }
2069
2631
 
2632
+ // Else apply it normally
2633
+ else
2634
+ path.apply(pathExprs[i]);
2070
2635
 
2071
- // Check again after we iterate through all paths to apply to a component.
2072
- if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
2073
- this.applyComponentExprs(lastNode, this.currentComponentProps);
2074
- this.currentComponentProps = {};
2075
- }
2076
2636
 
2637
+ } // end for(path of this.paths)
2638
+
2639
+
2640
+ // TODO: Only do this if we have ExprPaths within styles?
2077
2641
  this.updateStyles();
2078
2642
 
2079
2643
  // Invalidate the nodes cache because we just changed it.
@@ -2108,14 +2672,18 @@ class NodeGroup {
2108
2672
 
2109
2673
  // Call render() with the same params that would've been passed to the constructor.
2110
2674
  else if (el.render) {
2111
- let oldHash = Globals.componentHash.get(el);
2112
- if (oldHash !== newHash)
2113
- el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
2675
+ let oldHash = Globals$1.componentArgsHash.get(el);
2676
+ if (oldHash !== newHash) {
2677
+ let args = {};
2678
+ for (let name in props || {})
2679
+ args[Util.dashesToCamel(name)] = props[name];
2680
+ el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
2681
+ }
2114
2682
  }
2115
2683
 
2116
- Globals.componentHash.set(el, newHash);
2684
+ Globals$1.componentArgsHash.set(el, newHash);
2117
2685
  }
2118
-
2686
+
2119
2687
  /**
2120
2688
  * We swap the placeholder element for the real element so we can pass its dynamic attributes
2121
2689
  * to its constructor.
@@ -2129,72 +2697,48 @@ class NodeGroup {
2129
2697
  createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
2130
2698
  if (isPreHtmlElement === undefined)
2131
2699
  isPreHtmlElement = !el.hasAttribute('_is');
2132
-
2700
+
2133
2701
  let tagName = (isPreHtmlElement
2134
- ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
2135
- ? el.tagName.slice(0, -21)
2136
- : el.tagName
2702
+ ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
2137
2703
  : el.getAttribute('is')).toLowerCase();
2138
2704
 
2139
- let dynamicProps = {...(props || {})};
2140
-
2705
+
2706
+ // Throw if custom element isn't defined.
2707
+ let Constructor = customElements.get(tagName);
2708
+ if (!Constructor)
2709
+ throw new Error(`The custom tag name ${tagName} is not registered.`)
2710
+
2711
+ let args = {};
2712
+ for (let name in props || {})
2713
+ args[Util.dashesToCamel(name)] = props[name];
2714
+
2141
2715
  // Pass other attribs to constructor, since otherwise they're not yet set on the element,
2142
2716
  // and the constructor would otherwise have no way to see them.
2143
2717
  if (el.attributes.length) {
2144
- if (!props)
2145
- props = {};
2146
- for (let attrib of el.attributes)
2147
- if (!props.hasOwnProperty(attrib.name))
2148
- props[attrib.name] = attrib.value;
2718
+ for (let attrib of el.attributes) {
2719
+ let attribName = Util.dashesToCamel(attrib.name);
2720
+ if (!args.hasOwnProperty(attribName))
2721
+ args[attribName] = attrib.value;
2722
+ }
2149
2723
  }
2150
-
2151
- // Create CustomElement and
2152
- let Constructor = customElements.get(tagName);
2153
- if (!Constructor)
2154
- throw new Error(`The custom tag name ${tagName} is not registered.`)
2155
2724
 
2156
- // We pass the childNodes to the constructor so it can know about them,
2157
- // instead of only afterward when they're appended to the slot below.
2158
- // This is useful for a custom selectbox, for example.
2159
- // Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
2160
- // can add them as children before the rest of the constructor code executes.
2161
- let ch = [... el.childNodes];
2162
- Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
2163
- let newEl = new Constructor(props, ch);
2725
+ // Create the web component.
2726
+ // Get the children that aren't Solarite's comment placeholders.
2727
+ let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
2728
+ let newEl = new Constructor(args, ch);
2164
2729
 
2165
2730
  if (!isPreHtmlElement)
2166
2731
  newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
2732
+
2733
+ // Replace the placeholder tag with the instantiated web component.
2167
2734
  el.replaceWith(newEl);
2168
2735
 
2169
- // Set children / slot children
2170
- // TODO: Match named slots.
2171
- // TODO: This only appends to slot if render() is called in the constructor.
2172
- //let slot = newEl.querySelector('slot') || newEl;
2173
- //slot.append(...el.childNodes);
2174
-
2175
- // Copy over event attributes.
2176
- for (let propName in props) {
2177
- let val = props[propName];
2178
- if (propName.startsWith('on') && typeof val === 'function')
2179
- newEl.addEventListener(propName.slice(2), e => val(e, newEl));
2180
-
2181
- // Bind array based event attributes on value.
2182
- // This same logic is in ExprPath.applyValueAttrib() for non-components.
2183
- if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
2184
- let [obj, path] = [val[0], val.slice(1)];
2185
- newEl.value = delve(obj, path);
2186
- newEl.addEventListener('input', e => {
2187
- delve(obj, path, Util.getInputValue(newEl));
2188
- }, true); // We use capture so we update the values before other events added by the user.
2189
- }
2190
- }
2191
-
2192
2736
  // If an id pointed at the placeholder, update it to point to the new element.
2193
2737
  let id = el.getAttribute('data-id') || el.getAttribute('id');
2194
2738
  if (id)
2195
2739
  delve(this.getRootNode(), id.split(/\./g), newEl);
2196
-
2197
-
2740
+
2741
+
2198
2742
  // Update paths to use replaced element.
2199
2743
  for (let path of this.paths) {
2200
2744
  if (path.nodeMarker === el)
@@ -2206,31 +2750,31 @@ class NodeGroup {
2206
2750
  this.startNode = newEl;
2207
2751
  if (this.endNode === el)
2208
2752
  this.endNode = newEl;
2209
-
2210
-
2753
+
2754
+
2211
2755
  // applyComponentExprs() is called because we're rendering.
2212
2756
  // So we want to render the sub-component also.
2213
2757
  if (newEl.renderFirstTime)
2214
2758
  newEl.renderFirstTime();
2215
-
2759
+
2216
2760
  // Copy attributes over.
2217
2761
  for (let attrib of el.attributes)
2218
2762
  if (attrib.name !== '_is')
2219
2763
  newEl.setAttribute(attrib.name, attrib.value);
2220
2764
 
2221
2765
  // Set dynamic attributes if they are primitive types.
2222
- for (let name in dynamicProps) {
2223
- let val = dynamicProps[name];
2766
+ for (let name in props) {
2767
+ let val = props[name];
2224
2768
  if (typeof val === 'boolean') {
2225
2769
  if (val !== false && val !== undefined && val !== null)
2226
2770
  newEl.setAttribute(name, '');
2227
2771
  }
2228
2772
 
2229
- // If type isn't an object or array, set the attribute.
2773
+ // If type is a non-boolean primitive, set the attribute value.
2230
2774
  else if (['number', 'bigint', 'string'].includes(typeof val))
2231
2775
  newEl.setAttribute(name, val);
2232
2776
  }
2233
-
2777
+
2234
2778
  return newEl;
2235
2779
  }
2236
2780
 
@@ -2274,11 +2818,21 @@ class NodeGroup {
2274
2818
  return this.rootNg;
2275
2819
  }
2276
2820
 
2821
+ /**
2822
+ * Requires the nodeCache to be present. */
2823
+ removeAndSaveOrphans() {
2824
+
2825
+ let fragment = document.createDocumentFragment();
2826
+ for (let node of this.getNodes())
2827
+ fragment.append(node);
2828
+ }
2829
+
2277
2830
 
2278
2831
  updatePaths(fragment, paths, offset) {
2279
2832
  // Update paths to point to the fragment.
2280
- this.paths.length = paths.length;
2281
- for (let i=0; i<paths.length; i++) {
2833
+ let pathLength = paths.length;
2834
+ this.paths.length = pathLength;
2835
+ for (let i=0; i<pathLength; i++) {
2282
2836
  let path = paths[i].clone(fragment, offset);
2283
2837
  path.parentNg = this;
2284
2838
  this.paths[i] = path;
@@ -2296,61 +2850,70 @@ class NodeGroup {
2296
2850
 
2297
2851
 
2298
2852
 
2853
+ findStaticComponents(root, shell, pathOffset=0) {
2854
+ let result = [];
2299
2855
 
2300
- /**
2301
- * @param root {HTMLElement}
2302
- * @param shell {Shell}
2303
- * @param pathOffset {int} */
2304
- activateEmbeds(root, shell, pathOffset=0) {
2305
-
2306
- // static components. These are WebComponents not created by an expression.
2307
- // Must happen before ids.
2856
+ // static components. These are WebComponents that do not have any constructor arguments that are expressions.
2857
+ // Those are instead created by applyExpr() which calls applyComponentExprs() which calls createNewcomponent().
2858
+ // Maybe someday these two paths will be merged?
2859
+ // Must happen before ids because createNewComponent will replace the element.
2308
2860
  for (let path of shell.staticComponents) {
2309
2861
  if (pathOffset)
2310
2862
  path = path.slice(0, -pathOffset);
2311
2863
  let el = resolveNodePath(root, path);
2312
2864
 
2313
2865
  // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
2866
+ // Recreating it is necessary so we can pass the constructor args to it.
2314
2867
  if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
2315
- this.createNewComponent(el);
2868
+ result.push(el);
2316
2869
  }
2870
+ return result;
2871
+ }
2872
+
2873
+ activateStaticComponents(staticComponents) {
2874
+ for (let el of staticComponents)
2875
+ this.createNewComponent(el);
2876
+ }
2877
+
2878
+ /**
2879
+ * @param root {HTMLElement}
2880
+ * @param shell {Shell}
2881
+ * @param pathOffset {int} */
2882
+ activateEmbeds(root, shell, pathOffset=0) {
2317
2883
 
2318
2884
  let rootEl = this.rootNg.root;
2319
2885
  if (rootEl) {
2886
+ let options = this.rootNg.options;
2320
2887
 
2321
2888
  // ids
2322
- if (this.options?.ids !== false)
2889
+ if (options?.ids !== false) {
2323
2890
  for (let path of shell.ids) {
2324
2891
  if (pathOffset)
2325
2892
  path = path.slice(0, -pathOffset);
2326
2893
  let el = resolveNodePath(root, path);
2327
- let id = el.getAttribute('data-id') || el.getAttribute('id');
2328
- if (id) { // If something hasn't removed the id.
2329
-
2330
- // Don't allow overwriting existing class properties if they already have a non-Node value.
2331
- if (rootEl[id] && !(rootEl[id] instanceof Node))
2332
- throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
2333
- `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
2334
-
2335
- delve(rootEl, id.split(/\./g), el);
2336
- }
2894
+ Util.bindId(rootEl, el);
2895
+ }
2337
2896
  }
2338
2897
 
2339
2898
  // styles
2340
- if (this.options?.styles !== false) {
2899
+ if (options?.styles !== false) {
2341
2900
  if (shell.styles.length)
2342
2901
  this.styles = new Map();
2343
2902
  for (let path of shell.styles) {
2344
2903
  if (pathOffset)
2345
2904
  path = path.slice(0, -pathOffset);
2905
+
2906
+ /** @type {HTMLStyleElement} */
2346
2907
  let style = resolveNodePath(root, path);
2347
- Util.bindStyles(style, rootEl);
2348
- this.styles.set(style, style.textContent);
2908
+ if (rootEl.nodeType === 1) {
2909
+ Util.bindStyles(style, rootEl);
2910
+ this.styles.set(style, style.textContent);
2911
+ }
2349
2912
  }
2350
2913
 
2351
2914
  }
2352
2915
  // scripts
2353
- if (this.options?.scripts !== false) {
2916
+ if (options?.scripts !== false) {
2354
2917
  for (let path of shell.scripts) {
2355
2918
  if (pathOffset)
2356
2919
  path = path.slice(0, -pathOffset);
@@ -2370,6 +2933,11 @@ class RootNodeGroup extends NodeGroup {
2370
2933
  * @type {HTMLElement} */
2371
2934
  root;
2372
2935
 
2936
+ /**
2937
+ * When we call renerWatched() we re-render these expressions, then clear this to a new Map()
2938
+ * @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
2939
+ exprsToRender = new Map();
2940
+
2373
2941
  /**
2374
2942
  *
2375
2943
  * @param template
@@ -2384,73 +2952,102 @@ class RootNodeGroup extends NodeGroup {
2384
2952
  this.rootNg = this;
2385
2953
  let [fragment, shell] = this.init(template);
2386
2954
 
2387
- // If adding NodeGroup to an element.
2388
- let offset = 0;
2389
- let root = fragment; // TODO: Rename so it's not confused with this.root.
2390
- if (el) {
2955
+ if (fragment instanceof Text) {
2391
2956
 
2392
- // Save slot children
2393
- let slotFragment;
2394
- if (el.childNodes.length) {
2395
- slotFragment = document.createDocumentFragment();
2396
- slotFragment.append(...el.childNodes);
2957
+ if (el) {
2958
+ this.startNode = el;
2959
+ this.endNode = el;
2960
+ if (fragment.nodeValue.length)
2961
+ el.append(fragment);
2962
+ this.root = el;
2397
2963
  }
2964
+ Globals$1.nodeGroups.set(this.root, this);
2965
+ }
2966
+ else {
2398
2967
 
2399
- this.root = el;
2968
+ // If adding NodeGroup to an element.
2969
+ let offset = 0;
2970
+ let root = fragment; // TODO: Rename so it's not confused with this.root.
2971
+ if (el) {
2972
+ Globals$1.nodeGroups.set(el, this);
2973
+
2974
+ // Save slot children
2975
+ let slotChildren;
2976
+ if (el.childNodes.length) {
2977
+ slotChildren = document.createDocumentFragment();
2978
+ slotChildren.append(...el.childNodes);
2979
+ }
2400
2980
 
2401
- // If el should replace the root node of the fragment.
2402
- if (isReplaceEl(fragment, el)) {
2403
- el.append(...fragment.children[0].childNodes);
2981
+ this.root = el;
2404
2982
 
2405
- // Copy attributes
2406
- for (let attrib of fragment.children[0].attributes)
2407
- if (!el.hasAttribute(attrib.name))
2408
- el.setAttribute(attrib.name, attrib.value);
2983
+ // If el should replace the root node of the fragment.
2984
+ if (isReplaceEl(fragment, el)) {
2985
+ el.append(...fragment.children[0].childNodes);
2409
2986
 
2410
- // Go one level deeper into all of shell's paths.
2411
- offset = 1;
2412
- }
2413
- else {
2414
- let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2415
- if (!isEmpty)
2416
- el.append(...fragment.childNodes);
2417
- }
2987
+ // Copy attributes
2988
+ for (let attrib of fragment.children[0].attributes)
2989
+ if (!el.hasAttribute(attrib.name))
2990
+ el.setAttribute(attrib.name, attrib.value);
2418
2991
 
2419
- // Setup slots
2420
- if (slotFragment) {
2421
- for (let slot of el.querySelectorAll('slot[name]')) {
2422
- let name = slot.getAttribute('name');
2423
- if (name) {
2424
- let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
2425
- slot.append(...slotChildren);
2992
+ // Go one level deeper into all of shell's paths.
2993
+ offset = 1;
2994
+ } else {
2995
+ let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2996
+ if (!isEmpty)
2997
+ el.append(...fragment.childNodes);
2998
+ }
2999
+
3000
+ // Setup children
3001
+ if (slotChildren) {
3002
+
3003
+ // Named slots
3004
+ for (let slot of el.querySelectorAll('slot[name]')) {
3005
+ let name = slot.getAttribute('name');
3006
+ if (name) {
3007
+ let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
3008
+ slot.append(...slotChildren2);
3009
+ }
2426
3010
  }
3011
+
3012
+ // Unnamed slots
3013
+ let unamedSlot = el.querySelector('slot:not([name])');
3014
+ if (unamedSlot)
3015
+ unamedSlot.append(slotChildren);
3016
+
3017
+ // No slots
3018
+ else
3019
+ el.append(slotChildren);
2427
3020
  }
2428
- let unamedSlot = el.querySelector('slot:not([name])');
2429
- if (unamedSlot)
2430
- unamedSlot.append(slotFragment);
2431
- else
2432
- el.append(slotFragment);
2433
- }
2434
3021
 
2435
- root = el;
2436
- this.startNode = el;
2437
- this.endNode = el;
2438
- }
2439
- else {
2440
- let singleEl = getSingleEl(fragment);
2441
- this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
2442
- if (singleEl) {
2443
- root = singleEl;
2444
- offset = 1;
3022
+ root = el;
3023
+
3024
+ this.startNode = el;
3025
+ this.endNode = el;
3026
+ } else {
3027
+ let singleEl = getSingleEl(fragment);
3028
+ this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
3029
+
3030
+ Globals$1.nodeGroups.set(this.root, this);
3031
+ if (singleEl) {
3032
+ root = singleEl;
3033
+ offset = 1;
3034
+ }
2445
3035
  }
2446
- }
2447
3036
 
2448
- this.updatePaths(root, shell.paths, offset);
3037
+ this.updatePaths(root, shell.paths, offset);
2449
3038
 
2450
- this.activateEmbeds(root, shell, offset);
3039
+ // Static web components can sometimes have children created via expressions.
3040
+ // But calling applyExprs() will mess up the shell's path to them.
3041
+ // So we find them first, then call activateStaticComponents() after their children have been created.
3042
+ let staticComponents = this.findStaticComponents(root, shell, offset);
2451
3043
 
2452
- // Apply exprs
2453
- this.applyExprs(template.exprs);
3044
+ this.activateEmbeds(root, shell, offset);
3045
+
3046
+ // Apply exprs
3047
+ this.applyExprs(template.exprs);
3048
+
3049
+ this.activateStaticComponents(staticComponents);
3050
+ }
2454
3051
  }
2455
3052
  }
2456
3053
 
@@ -2472,8 +3069,8 @@ function getSingleEl(fragment) {
2472
3069
  * @param el {HTMLElement}
2473
3070
  * @returns {boolean} */
2474
3071
  function isReplaceEl(fragment, el) {
2475
- return el.tagName.includes('-')
2476
- && fragment.children.length===1
3072
+ return fragment.children.length===1
3073
+ && el.tagName.includes('-')
2477
3074
  && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
2478
3075
  }
2479
3076
 
@@ -2492,19 +3089,9 @@ class Template {
2492
3089
  /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
2493
3090
  hashedFields;
2494
3091
 
2495
- /**
2496
- * @deprecated
2497
- * @type {ExprPath} Used with forEach() from watch.js
2498
- * Set in ExprPath.apply() */
2499
- parentPath;
2500
-
2501
3092
  /** @type {NodeGroup} */
2502
3093
  nodeGroup;
2503
3094
 
2504
- /**
2505
- * @type {string[][]} */
2506
- paths = [];
2507
-
2508
3095
  /**
2509
3096
  *
2510
3097
  * @param htmlStrings {string[]}
@@ -2546,39 +3133,54 @@ class Template {
2546
3133
  if (standalone) {
2547
3134
  ng = new RootNodeGroup(this, null, options);
2548
3135
  el = ng.getRootNode();
2549
- Globals.nodeGroups.set(el, ng);
3136
+ Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
2550
3137
  firstTime = true;
2551
3138
  }
2552
3139
  else {
2553
- ng = Globals.nodeGroups.get(el);
3140
+ ng = Globals$1.nodeGroups.get(el);
2554
3141
  if (!ng) {
2555
3142
  ng = new RootNodeGroup(this, el, options);
2556
- Globals.nodeGroups.set(el, ng);
3143
+ Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
2557
3144
  firstTime = true;
2558
3145
  }
2559
- }
3146
+
3147
+ // This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
3148
+ // These don't always have the same length, for example if one attribute has multiple expressions.
3149
+ if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
3150
+ throw new Error(`Solarite Error: Parent HTMLElement ${ng.template.html.join('${...}')} and ${ng.paths.length} \${value} placeholders can't accomodate a Template with ${this.exprs.length} values.`); }
2560
3151
 
2561
3152
  // Creating the root nodegroup also renders it.
2562
3153
  // If we didn't just create it, we need to render it.
2563
3154
  if (!firstTime) {
2564
3155
  if (this.html?.length === 1 && !this.html[0])
2565
3156
  el.innerHTML = ''; // Fast path for empty component.
2566
- else
3157
+ else {
2567
3158
  ng.applyExprs(this.exprs);
3159
+ }
2568
3160
  }
2569
3161
 
3162
+ ng.exprsToRender = new Map();
2570
3163
  return el;
2571
3164
  }
2572
3165
 
2573
3166
  getExactKey() {
2574
- if (!this.exactKey)
2575
- this.exactKey = getObjectHash(this); // calls this.toJSON().
3167
+ if (!this.exactKey) {
3168
+ if (this.exprs.length)
3169
+ this.exactKey = getObjectHash(this);// calls this.toJSON().
3170
+ else // Don't hash plain html.
3171
+ this.exactKey = this.html[0];
3172
+ }
2576
3173
  return this.exactKey;
2577
3174
  }
2578
3175
 
2579
3176
  getCloseKey() {
2580
- if (!this.closeKey)
2581
- this.closeKey = '@'+this.toJSON()[0];
3177
+ //console.log(this.exprs.length)
3178
+ if (!this.closeKey) {
3179
+ if (this.exprs.length)
3180
+ this.closeKey = /*'@' + */this.toJSON()[0];
3181
+ else
3182
+ this.closeKey = this.html[0];
3183
+ }
2582
3184
  // Use the joined html when debugging? But it breaks some tests.
2583
3185
  //return '@'+this.html.join('|')
2584
3186
 
@@ -2601,8 +3203,8 @@ class Template {
2601
3203
 
2602
3204
  /**
2603
3205
  * Convert strings to HTMLNodes.
2604
- * Using r as a tag will always create a Template.
2605
- * Using r() as a function() will always create a DOM element.
3206
+ * Using h`...` as a tag will always create a Template.
3207
+ * Using h() as a function() will always create a DOM element.
2606
3208
  *
2607
3209
  * Features beyond what standard js tagged template strings do:
2608
3210
  * 1. r`` sub-expressions
@@ -2612,24 +3214,25 @@ class Template {
2612
3214
  * 5. TODO: list more
2613
3215
  *
2614
3216
  * Currently supported:
2615
- * 1. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
2616
- * 2. r(el, template, ?options) // Render the Template created by #1 to element.
3217
+ * 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
3218
+ * 2. h(el, template, ?options) // Render the Template created by #1 to element.
2617
3219
  *
2618
- * 3. r`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
3220
+ * 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
2619
3221
  *
2620
- * 4. r('Hello'); // Create single text node.
2621
- * 5. r('<b>Hello</b>'); // Create single HTMLElement
2622
- * 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
2623
- * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
3222
+ * 4. h('Hello'); // Create single text node.
3223
+ * 5. h('<b>Hello</b>'); // Create single HTMLElement
3224
+ * 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3225
+ * 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
2624
3226
  * // includes properly handling nested components and r`` sub-expressions.
2625
- * 8. r(template) // Render Template created by #1.
2626
- *
2627
- * 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3227
+ * 8. h(template) // Render Template created by #1.
2628
3228
  *
3229
+ * 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3230
+ * 10. h(string, object, ...) // JSX TODO
2629
3231
  * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
2630
3232
  * @param exprs {*[]|string|Template|Object}
2631
3233
  * @return {Node|HTMLElement|Template} */
2632
- function r(htmlStrings=undefined, ...exprs) {
3234
+ function h(htmlStrings=undefined, ...exprs) {
3235
+
2633
3236
 
2634
3237
  // TODO: Make this a more flat if/else and call other functions for the logic.
2635
3238
  if (htmlStrings instanceof Node) {
@@ -2644,7 +3247,7 @@ function r(htmlStrings=undefined, ...exprs) {
2644
3247
 
2645
3248
  // Return a tagged template function that applies the tagged themplate to parent.
2646
3249
  let taggedTemplate = (htmlStrings, ...exprs) => {
2647
- Globals.rendered.add(parent);
3250
+ Globals$1.rendered.add(parent);
2648
3251
  let template = new Template(htmlStrings, exprs);
2649
3252
  return template.render(parent, options);
2650
3253
  };
@@ -2681,6 +3284,22 @@ function r(htmlStrings=undefined, ...exprs) {
2681
3284
  }
2682
3285
 
2683
3286
  else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
3287
+ // 10. JSX
3288
+ if (typeof exprs[0] === 'object') {
3289
+ exprs[0] || {};
3290
+ exprs.slice(1);
3291
+
3292
+ let templateHtmlStrings = [];
3293
+ let templateExprs = [];
3294
+
3295
+ // TODO How to know which children are static html and which are expression placeholders?
3296
+ // Perhaps we have to treat every text child as a string?
3297
+
3298
+ assert(templateHtmlStrings.length === templateExprs.length+1);
3299
+ return new Template(templateHtmlStrings, templateExprs);
3300
+ }
3301
+
3302
+
2684
3303
  // If it starts with a string, trim both ends.
2685
3304
  // TODO: Also trim if it ends with whitespace?
2686
3305
  if (htmlStrings.match(/^\s^</))
@@ -2704,7 +3323,7 @@ function r(htmlStrings=undefined, ...exprs) {
2704
3323
  else if (htmlStrings === undefined) {
2705
3324
  return (htmlStrings, ...exprs) => {
2706
3325
  //Globals.rendered.add(parent)
2707
- let template = r(htmlStrings, ...exprs);
3326
+ let template = h(htmlStrings, ...exprs);
2708
3327
  return template.render();
2709
3328
  }
2710
3329
  }
@@ -2716,192 +3335,168 @@ function r(htmlStrings=undefined, ...exprs) {
2716
3335
 
2717
3336
 
2718
3337
  // 9. Create dynamic element with render() function.
3338
+ // TODO: This path doesn't handle embeds like data-id="..."
2719
3339
  else if (typeof htmlStrings === 'object') {
2720
3340
  let obj = htmlStrings;
2721
3341
 
3342
+ if (obj.constructor.name !== 'Object')
3343
+ throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
3344
+
3345
+
2722
3346
  // Special rebound render path, called by normal path.
2723
- if (Globals.objToEl.has(obj)) {
3347
+ // Intercepts the main r`...` function call inside render().
3348
+ if (Globals$1.objToEl.has(obj)) {
2724
3349
  return function(...args) {
2725
- let template = r(...args);
3350
+ let template = h(...args);
2726
3351
  let el = template.render();
2727
- Globals.objToEl.set(obj, el);
3352
+ Globals$1.objToEl.set(obj, el);
2728
3353
  }.bind(obj);
2729
3354
  }
2730
3355
 
2731
3356
  // Normal path
2732
3357
  else {
2733
- Globals.objToEl.set(obj, null);
2734
- obj.render(); // Calls the Special rebound render path above, when the render function calls r(this)
2735
- let el = Globals.objToEl.get(obj);
2736
- Globals.objToEl.delete(obj);
3358
+ Globals$1.objToEl.set(obj, null);
3359
+ obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
3360
+ let el = Globals$1.objToEl.get(obj);
3361
+ Globals$1.objToEl.delete(obj);
2737
3362
 
2738
3363
  for (let name in obj)
2739
3364
  if (typeof obj[name] === 'function')
2740
- el[name] = obj[name].bind(el);
3365
+ el[name] = obj[name].bind(el); // Make the "this" of functions be el.
3366
+ // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
3367
+ // <my-element arg=${{myFunc() { return this }}}
2741
3368
  else
2742
3369
  el[name] = obj[name];
2743
3370
 
3371
+ // Bind id's
3372
+ // This doesn't work for id's referenced by attributes.
3373
+ // for (let idEl of el.querySelectorAll('[id],[data-id]')) {
3374
+ // Util.bindId(el, idEl);
3375
+ // Util.bindId(obj, idEl);
3376
+ // }
3377
+ // TODO: Bind styles
3378
+
2744
3379
  return el;
2745
3380
  }
2746
3381
  }
2747
3382
 
2748
3383
  else
2749
3384
  throw new Error('Unsupported arguments.')
2750
- }
2751
-
2752
- //import {watchGet, watchSet} from "./watch.js";
2753
-
2754
-
2755
-
2756
- function defineClass(Class, tagName, extendsTag) {
2757
- if (!customElements.getName(Class)) { // If not previously defined.
2758
- tagName = tagName || camelToDashes(Class.name);
2759
- if (!tagName.includes('-'))
2760
- tagName += '-element';
2761
-
2762
- let options = null;
2763
- if (extendsTag)
2764
- options = {extends: extendsTag};
2765
-
2766
- customElements.define(tagName, Class, options);
2767
- }
2768
3385
  }
2769
3386
 
2770
-
2771
-
2772
-
2773
-
3387
+ // Trick to prevent minifier from renaming this function.
3388
+ let renderF = 'render';
3389
+
2774
3390
  /**
2775
- * Create a version of the Solarite class that extends from the given tag name.
2776
- * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
2777
- * 1. customElements.define() is called automatically when you create the first instance.
2778
- * 2. Calls render() when added to the DOM, if it hasn't been called already.
2779
- * 3. Child elements are added before constructor is called. But they're also passed to the constructor.
2780
- * 4. We can use this.html = r`...` to set html.
2781
- * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
2782
- * Can't figure out how to have these work standalone though, and still be synchronous.
2783
- * 6. Can we extend from other element types like TR?
2784
- * 7. Shows default text if render() function isn't defined.
3391
+ * There are three ways to create an instance of a Solarite Component:
3392
+ * 1. new ComponentName(); // direct class instantiation
3393
+ * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
3394
+ * 3. <body><component-name></component-name></body> // in the Document html.
2785
3395
  *
2786
- * Advantages to inheriting from HTMLElement
2787
- * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
2788
- * 2. We can inherit from things like HTMLTableRowElement directly.
2789
- * 3. There's less magic, since everyone is familiar with defining custom elements.
3396
+ * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
3397
+ * sure we get the correct value via all three paths, we write our constructors according to the following
3398
+ * example. Note that constructor args are embedded in an object, and must be all lower-case because
3399
+ * Browsers make all html attribute names lowercase.
2790
3400
  *
2791
- * @param extendsTag {?string}
2792
- * @return {Class} */
2793
- function createSolarite(extendsTag=null) {
2794
-
2795
- let BaseClass = HTMLElement;
2796
- if (extendsTag && !extendsTag.includes('-')) {
2797
- extendsTag = extendsTag.toLowerCase();
2798
-
2799
- BaseClass = Globals.elementClasses[extendsTag];
2800
- if (!BaseClass) { // TODO: Use Cache
2801
- BaseClass = document.createElement(extendsTag).constructor;
2802
- Globals.elementClasses[extendsTag] = BaseClass;
2803
- }
2804
- }
2805
-
2806
- /**
2807
- * Intercept the construct call to auto-define the class before the constructor is called.
2808
- * @type {HTMLElement} */
2809
- let HTMLElementAutoDefine = new Proxy(BaseClass, {
2810
- construct(Parent, args, Class) {
2811
- defineClass(Class, null, extendsTag);
2812
-
2813
- // This is a good place to manipulate any args before they're sent to the constructor.
2814
- // Such as loading them from attributes, if I could find a way to do so.
2815
-
2816
- // This line is equivalent the to super() call.
2817
- return Reflect.construct(Parent, args, Class);
2818
- }
2819
- });
2820
-
2821
- return class Solarite extends HTMLElementAutoDefine {
2822
-
2823
-
2824
- /**
2825
- * TODO: Make these standalone functions.
2826
- * Callbacks.
2827
- * Use onConnect.push(() => ...); to add new callbacks. */
2828
- onConnect = Util$1.callback();
3401
+ * @example
3402
+ * constructor({name, userid=1}={}) {
3403
+ * super();
3404
+ *
3405
+ * // Get value from "name" attriute if persent, otherwise from name constructor arg.
3406
+ * this.name = getArg(this, 'name', name);
3407
+ *
3408
+ * // Optionally convert the value to an integer.
3409
+ * this.userId = getArg(this, 'userid', userid, ArgType.Int);
3410
+ * }
3411
+ *
3412
+ * @param el {HTMLElement}
3413
+ * @param attributeName {string} Attribute name. Not case-sensitive.
3414
+ * @param defaultValue {*} Default value to use if attribute doesn't exist.
3415
+ * @param type {ArgType|function|*[]}
3416
+ * If an array, use the value if it's in the array, otherwise return undefined.
3417
+ * If it's a function, pass the value to the function and return the result.
3418
+ * @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
3419
+ * TODO: Should this be merged with the defaultValue argument?
3420
+ * @return {*} Undefined if attribute isn't set. */
3421
+ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
3422
+ let val = defaultValue;
3423
+ let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
3424
+ if (attrVal !== null) // If attribute doesn't exist.
3425
+ val = attrVal;
2829
3426
 
2830
- onFirstConnect = Util$1.callback();
2831
- onDisconnect = Util$1.callback();
2832
-
2833
- /**
2834
- * @param options {RenderOptions} */
2835
- constructor(options={}) {
2836
- super();
2837
-
2838
- // TODO: Is options.render ever used?
2839
- if (options.render===true)
2840
- this.render();
2841
-
2842
- else if (options.render===false)
2843
- Globals.rendered.add(this); // Don't render on connectedCallback()
2844
-
2845
- // Add children before constructor code executes.
2846
- // PendingChildren is setup in NodeGroup.createNewComponent()
2847
- // TODO: Match named slots.
2848
- let ch = Globals.pendingChildren.pop();
2849
- if (ch)
2850
- (this.querySelector('slot') || this).append(...ch);
2851
-
2852
- /** @deprecated */
2853
- Object.defineProperty(this, 'html', {
2854
- set(html) {
2855
- Globals.rendered.add(this);
2856
- if (typeof html === 'string') {
2857
- console.warn("Assigning to this.html without the r template prefix.");
2858
- this.innerHTML = html;
2859
- }
3427
+ if (Array.isArray(type))
3428
+ return type.includes(val) ? val : fallback;
3429
+
3430
+ if (typeof type === 'function')
3431
+ return type(val);
3432
+
3433
+ // If bool, it's true as long as it exists and its value isn't falsey.
3434
+ if (type===ArgType.Bool) {
3435
+ let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
3436
+ if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
3437
+ return false;
3438
+ if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
3439
+ return true;
3440
+ return fallback;
3441
+ }
3442
+
3443
+ // Attribute doesn't exist
3444
+ let result;
3445
+ switch (type) {
3446
+ case ArgType.Int:
3447
+ result = parseInt(val);
3448
+ return isNaN(result) ? fallback : result;
3449
+ case ArgType.Float:
3450
+ result = parseFloat(val);
3451
+ return isNaN(result) ? fallback : result;
3452
+ case ArgType.String:
3453
+ return [undefined, null, false].includes(val) ? '' : val+'';
3454
+ case ArgType.Json:
3455
+ case ArgType.Eval:
3456
+ if (typeof val === 'string' && val.length)
3457
+ try {
3458
+ if (type === ArgType.Json)
3459
+ return JSON.parse(val);
2860
3460
  else
2861
- this.modifications = r(this, html, options);
3461
+ return eval(`(${val})`);
3462
+ } catch (e) {
3463
+ return val;
2862
3464
  }
2863
- });
3465
+ else return val;
2864
3466
 
2865
- /*
2866
- let pthis = new Proxy(this, {
2867
- get(obj, prop) {
2868
- return Reflect.get(obj, prop)
2869
- }
2870
- });
2871
- this.render = this.render.bind(pthis);
2872
- */
2873
- }
3467
+ // type not provided
3468
+ default:
3469
+ return val;
3470
+ }
3471
+ }
2874
3472
 
2875
- /**
2876
- * Call render() only if it hasn't already been called. */
2877
- renderFirstTime() {
2878
- if (!Globals.rendered.has(this) && this.render)
2879
- this.render();
2880
- }
2881
-
2882
- /**
2883
- * Called automatically by the browser. */
2884
- connectedCallback() {
2885
- this.renderFirstTime();
2886
- if (!Globals.connected.has(this)) {
2887
- Globals.connected.add(this);
2888
- this.onFirstConnect();
2889
- }
2890
- this.onConnect();
2891
- }
2892
-
2893
- disconnectedCallback() {
2894
- this.onDisconnect();
2895
- }
3473
+ /**
3474
+ * @enum */
3475
+ var ArgType = {
3476
+
3477
+ /**
3478
+ * false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
3479
+ * Anything else, including empty string becomes true.
3480
+ * Empty string is true because attributes with no value should be evaulated as true. */
3481
+ Bool: 'Bool',
3482
+
3483
+ Int: 'Int',
3484
+ Float: 'Float',
3485
+ String: 'String',
2896
3486
 
3487
+ /** @deprecated for Json */
3488
+ JSON: 'Json',
2897
3489
 
2898
- static define(tagName=null) {
2899
- defineClass(this, tagName, extendsTag);
2900
- }
3490
+ /**
3491
+ * Parse the string value as JSON.
3492
+ * If it's not parsable, return the value as a string. */
3493
+ Json: 'Json',
2901
3494
 
2902
-
2903
- }
2904
- }
3495
+ /**
3496
+ * Evaluate the string as JavaScript using the eval() function.
3497
+ * If it can't be evaluated, return the original string. */
3498
+ Eval: 'Eval'
3499
+ };
2905
3500
 
2906
3501
  /**
2907
3502
  * Solarite JavasCript UI library.
@@ -2920,7 +3515,7 @@ let Solarite = new Proxy(createSolarite(), {
2920
3515
  let getInputValue = Util.getInputValue;
2921
3516
 
2922
3517
  //Experimental:
2923
- //export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
2924
- //export {watch} from './watch2.js'; // unfinished
3518
+ //export {default as watch, renderWatched} from './watch.js'; // unfinished
2925
3519
 
2926
- export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r };
3520
+ export default h;
3521
+ export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, getInputValue, h, h as r };