solarite 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/Solarite-debug.js +1457 -1402
  2. package/dist/Solarite.js +1425 -1272
  3. package/dist/Solarite.min.js +2 -2
  4. package/package.json +5 -6
  5. package/readme.md +2 -4
  6. package/src/{solarite/ExprPath.js → ExprPath.js} +421 -231
  7. package/src/Globals.js +79 -0
  8. package/src/HtmlParser.js +91 -0
  9. package/src/{util/MultiValueMap.js → MultiValueMap.js} +22 -26
  10. package/src/{solarite/NodeGroup.js → NodeGroup.js} +286 -226
  11. package/src/{solarite/Shell.js → Shell.js} +119 -92
  12. package/src/Solarite.d.ts +62 -0
  13. package/src/{solarite/Solarite.js → Solarite.js} +15 -13
  14. package/src/{solarite/Template.js → Template.js} +22 -19
  15. package/src/Util.js +330 -0
  16. package/src/{util/Errors.js → assert.js} +1 -0
  17. package/src/createSolarite.js +154 -0
  18. package/src/{util/delve.js → delve.js} +5 -4
  19. package/src/{solarite/getArg.js → getArg.js} +41 -15
  20. package/src/{solarite/r.js → h.js} +59 -29
  21. package/src/{solarite/hash.js → hash.js} +12 -9
  22. package/src/unused/FastLookupArray.js +54 -0
  23. package/src/unused/Hashes.js +339 -0
  24. package/src/unused/InUse.test.js +92 -0
  25. package/src/unused/InUseMap.js +98 -0
  26. package/src/unused/LinkedList.js +117 -0
  27. package/src/unused/LinkedList.test.js +115 -0
  28. package/src/unused/Misc.js +13 -0
  29. package/src/unused/Perf.js +47 -0
  30. package/src/unused/TrackedArray.js +54 -0
  31. package/src/watch.js +546 -0
  32. package/src/solarite/Globals.js +0 -54
  33. package/src/solarite/Util.js +0 -388
  34. package/src/solarite/createSolarite.js +0 -274
  35. package/src/solarite/watch3.js +0 -189
  36. package/src/util/Util.js +0 -113
  37. /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
  38. /package/src/{util → unused}/WeakArray.js +0 -0
@@ -1,261 +1,13 @@
1
- /**
2
- * @typedef {Array|function(...*)} Callbacks
3
- * @property {function(function)} push
4
- * @property {function()} remove
5
- * @property {function()} pause
6
- * @property {function()} resume
7
- * */
8
-
9
-
10
- /**
11
- * A place for functions that have no other home. */
12
- var Util$1 = {
13
-
14
- /**
15
- * Create an array-like object that stores a group of callbacks.
16
- * Supports all array functions and properties like push() and .length.
17
- * Can be called directly.
18
- *
19
- * @param functions {function[]}
20
- * @return {Callbacks|function}
21
- *
22
- * @example
23
- * var c = Util.callback();
24
- * var f = () => console.log(3);
25
- * c.push(f);
26
- * c();
27
- * c.remove(f);
28
- * c();
29
- */
30
- callback(...functions) {
31
- var paused = false;
32
-
33
- // Make it callable. When we call it, call all callbacks() with the given args.
34
- let result = async function(...args) {
35
- let result2 = [];
36
- if (!paused)
37
- for (let i=0; i<result.length; i++)
38
- result2.push(result[i](...args));
39
- return await Promise.all(result2);
40
- };
41
-
42
- // Make it iterable.
43
- result[Symbol.iterator] = function() {
44
- let index = 0;
45
- return {
46
- next: () => index < result.length
47
- ? {value: result[index++], done: false}
48
- : {done: true}
49
- };
50
- };
51
-
52
- // Use properties from Array
53
- for (let prop of Object.getOwnPropertyNames(Array.prototype))
54
- if (prop !== 'length' && prop !== 'constructor')
55
- result[prop] = Array.prototype[prop];
56
-
57
- result.l = 0; // Internal length
58
- Object.defineProperty(result, 'length', {
59
- get() { return result.l },
60
- set(val) { result.l = val;}
61
- });
62
-
63
- // Add the remove() function.
64
- result.remove = func => {
65
- let idx = result.findIndex(item => item === func);
66
- if (idx !== -1)
67
- result.splice(idx, 1);
68
- };
69
- result.pause = () => paused = true;
70
-
71
- result.resume = () => paused = false;
72
-
73
- // Add initial functions
74
- for (let f of functions)
75
- result.push(f);
76
-
77
- return result;
78
- },
79
-
80
- /**
81
- * @param map {Map|WeakMap|Object}
82
- * @param key
83
- * @param value */
84
- mapAdd(map, key, value) {
85
- let isMap = map instanceof Map || map instanceof WeakMap;
86
- let result = isMap ? map.get(key) : map[key];
87
- if (!result) {
88
- result = [value];
89
- if (isMap)
90
- map.set(key, result);
91
- else
92
- map[key] = result;
93
- }
94
- else
95
- result.push(value);
96
- },
97
-
98
- weakMemoize(obj, callback) {
99
- let result = weakMemoizeInputs.get(obj);
100
- if (!result) {
101
- result = callback(obj);
102
- weakMemoizeInputs.set(obj, result);
103
- }
104
- return result;
105
- }
106
- };
107
-
108
- let weakMemoizeInputs = new WeakMap();
109
-
110
- /**
111
- * Follow a path into an object.
112
- * @param obj {object}
113
- * @param path {string[]}
114
- * @param createVal {*} If set, non-existant paths will be created and value at path will be set to createVal.
115
- * @return {*} The value, or undefined if it can't be reached. */
116
- function delve(obj, path, createVal = delveDontCreate) {
117
- let isCreate = createVal !== delveDontCreate;
118
-
119
- let len = path.length;
120
- if (!obj && !isCreate && len)
121
- return undefined;
122
-
123
- let i = 0;
124
- for (let srcProp of path) {
125
-
126
- // If the path is undefined and we're not to the end yet:
127
- if (obj[srcProp] === undefined) {
128
-
129
- // If the next index is an integer or integer string.
130
- if (isCreate) {
131
- if (i < len - 1) {
132
- // If next level path is a number, create as an array
133
- let isArray = (path[i + 1] + '').match(/^\d+$/);
134
- obj[srcProp] = isArray ? [] : {};
135
- }
136
- } else
137
- return undefined; // can't traverse
138
- }
139
-
140
- // If last item in path
141
- if (isCreate && i === len - 1)
142
- obj[srcProp] = createVal;
143
-
144
- // Traverse deeper along destination object.
145
- obj = obj[srcProp];
146
- i++;
147
- }
148
-
149
- return obj;
150
- }
151
-
152
- let delveDontCreate = {};
153
-
154
- /**
155
- * There are three ways to create an instance of a Solarite Component:
156
- * 1. new ComponentName(); // direct class instantiation
157
- * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
158
- * 3. <body><component-name></component-name></body> // in the Document html.
159
- *
160
- * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
161
- * sure we get the correct value via all three paths, we write our constructors according to the following
162
- * example. Note that constructor args are embedded in an object, and must be all lower-case because
163
- * Browsers make all html attribute names lowercase.
164
- *
165
- * @example
166
- * constructor({name, userid=1}={}) {
167
- * super();
168
- *
169
- * // Get value from "name" attriute if persent, otherwise from name constructor arg.
170
- * this.name = getArg(this, 'name', name);
171
- *
172
- * // Optionally convert the value to an integer.
173
- * this.userId = getArg(this, 'userid', userid, ArgType.Int);
174
- * }
175
- *
176
- * @param el {HTMLElement}
177
- * @param name {string} Attribute name. Not case-sensitive.
178
- * @param val {*} Default value to use if attribute doesn't exist.
179
- * @param type {ArgType|function|*[]}
180
- * If an array, use the value if it's in the array, otherwise return undefined.
181
- * If it's a function, pass the value to the function and return the result.
182
- * @param fallback {*} If the type can't be parsed as the given type, use this value.
183
- * @return {*} Undefined if attribute isn't set. */
184
- function getArg(el, name, val=undefined, type=ArgType.String, fallback=undefined) {
185
- let attrVal = el.getAttribute(name);
186
- if (attrVal !== null) // If attribute doesn't exist.
187
- val = attrVal;
188
-
189
- if (Array.isArray(type))
190
- return type.includes(val) ? val : fallback;
191
-
192
- if (typeof type === 'function')
193
- return type(val);
194
-
195
- // If bool, it's true as long as it exists and its value isn't falsey.
196
- if (type===ArgType.Bool) {
197
- let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
198
- if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
199
- return false;
200
- if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
201
- return true;
202
- return fallback;
203
- }
204
-
205
- // Attribute doesn't exist
206
- let result;
207
- switch (type) {
208
- case ArgType.Int:
209
- result = parseInt(val);
210
- return isNaN(result) ? fallback : result;
211
- case ArgType.Float:
212
- result = parseFloat(val);
213
- return isNaN(result) ? fallback : result;
214
- case ArgType.String:
215
- return [undefined, null, false].includes(val) ? '' : val+'';
216
- case ArgType.JSON:
217
- case ArgType.Eval:
218
- if (typeof val === 'string' && val.length)
219
- try {
220
- if (type === ArgType.JSON)
221
- return JSON.parse(val);
222
- else
223
- return eval(`(${val})`);
224
- } catch (e) {
225
- return val;
226
- }
227
- else return fallback;
228
-
229
- // type not provided
230
- default:
231
- return val;
1
+ //#IFDEV
2
+ /*@__NO_SIDE_EFFECTS__*/
3
+ function assert(val) {
4
+ if (!val) {
5
+ debugger;
6
+ throw new Error('Assertion failed: ' + val);
232
7
  }
233
8
  }
234
9
 
235
- /**
236
- * @enum */
237
- var ArgType = {
238
-
239
- /**
240
- * false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
241
- * Anything else, including empty string becomes true.
242
- * Empty string is true because attributes with no value should be evaulated as true. */
243
- Bool: 'Bool',
244
-
245
- Int: 'Int',
246
- Float: 'Float',
247
- String: 'String',
248
-
249
- /**
250
- * Parse the string value as JSON.
251
- * If it's not parsable, return the value as a string. */
252
- JSON: 'JSON',
253
-
254
- /**
255
- * Evaluate the string as JavaScript using the eval() function.
256
- * If it can't be evaluated, return the original string. */
257
- Eval: 'Eval'
258
- };
10
+ //#ENDIF
259
11
 
260
12
  let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
261
13
  let objectIds = new WeakMap();
@@ -269,7 +21,7 @@ function getObjectId(obj) {
269
21
 
270
22
  let result = objectIds.get(obj);
271
23
  if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
272
- result = (lastObjectId++); // We use a unique prefix to ensure it doesn't collide w/ strings not from getObjectId()
24
+ result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
273
25
  objectIds.set(obj, result);
274
26
  }
275
27
  return result;
@@ -288,14 +40,7 @@ function toJSON() {
288
40
 
289
41
  // Node.prototype.toJSON = toJSON;
290
42
  // Function.prototype.toJSON = toJSON;
291
- // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
292
- // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
293
- // So we check the assignments on every run of getObjectHash()
294
- if (Node.prototype.toJSON !== toJSON) {
295
- Node.prototype.toJSON = toJSON;
296
- if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
297
- Function.prototype.toJSON = toJSON;
298
- }
43
+
299
44
 
300
45
  /**
301
46
  * Get a string that uniquely maps to the values of the given object.
@@ -310,6 +55,16 @@ if (Node.prototype.toJSON !== toJSON) {
310
55
  * @param obj {*}
311
56
  * @returns {string} */
312
57
  function getObjectHash(obj) {
58
+
59
+ // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
60
+ // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
61
+ // So we check the assignments on every run of getObjectHash()
62
+ if (Node.prototype.toJSON !== toJSON) {
63
+ Node.prototype.toJSON = toJSON;
64
+ if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
65
+ Function.prototype.toJSON = toJSON;
66
+ }
67
+
313
68
  let result;
314
69
  isHashing = true;
315
70
  try {
@@ -342,71 +97,164 @@ function getObjectHashCircular(obj) {
342
97
  });
343
98
  }
344
99
 
345
- //#IFDEV
346
- /*@__NO_SIDE_EFFECTS__*/
347
- function assert(val) {
348
- if (!val) {
349
- debugger;
350
- throw new Error('Assertion failed: ' + val);
351
- }
100
+ var Globals;
101
+
102
+ /**
103
+ * Created with a reset() function because it's useful for testing. */
104
+ function reset() {
105
+ Globals = {
106
+
107
+ /**
108
+ * Used by NodeGroup.applyComponentExprs() */
109
+ componentArgsHash: new WeakMap(),
110
+
111
+ /**
112
+ * Store which instances of Solarite have already been added to the DOM.
113
+ * @type {WeakSet<HTMLElement>} */
114
+ connected: new WeakSet(),
115
+
116
+ /**
117
+ * ExprPath.applyExactNodes() sets this property when an expression is being accessed.
118
+ * watch() then adds the ExprPath to the list of ExprPaths that should be re-rendered when the value changes.
119
+ * @type {ExprPath}*/
120
+ currentExprPath: null,
121
+
122
+ div: document.createElement("div"),
123
+
124
+ /**
125
+ * @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
126
+ elementClasses: {},
127
+
128
+ /** @type {Record<string, boolean>} Key is tag-name.propName. Value is whether it's an attribute.*/
129
+ htmlProps: {},
130
+
131
+ /**
132
+ * Used by ExprPath.applyEventAttrib()
133
+ * @type {WeakMap<Node, Record<eventName:string, [original:function, bound:function, args:*[]]>>} */
134
+ nodeEvents: new WeakMap(),
135
+
136
+ /**
137
+ * Get the RootNodeGroup for an element.
138
+ * @type {WeakMap<HTMLElement, RootNodeGroup>} */
139
+ nodeGroups: new WeakMap(),
140
+
141
+ /**
142
+ * Used by r() path 9. */
143
+ objToEl: new WeakMap(),
144
+
145
+ //pendingChildren: [],
146
+
147
+
148
+ /**
149
+ * Elements that have been rendered to by r() at least once.
150
+ * This is used by the Solarite class to know when to call onFirstConnect()
151
+ * @type {WeakSet<HTMLElement>} */
152
+ rendered: new WeakSet(),
153
+
154
+ /**
155
+ * Elements that are currently rendering via the r() function.
156
+ * @type {WeakSet<HTMLElement>} */
157
+ rendering: new WeakSet(),
158
+
159
+ /**
160
+ * Map from array of Html strings to a Shell created from them.
161
+ * @type {WeakMap<string[], Shell>} */
162
+ shells: new WeakMap(),
163
+
164
+ /**
165
+ * A map of individual untagged strings to their Templates.
166
+ * This way we don't keep creating new Templates for the same string when re-rendering.
167
+ * This is used by ExprPath.applyExactNodes()
168
+ * @type {Record<string, Template>} */
169
+ //stringTemplates: {},
170
+
171
+ reset,
172
+
173
+ count: 0
174
+ };
352
175
  }
353
- //#ENDIF
176
+ reset();
177
+
178
+ var Globals$1 = Globals;
354
179
 
355
- var Globals = {
180
+ /**
181
+ * Follow a path into an object.
182
+ * @param obj {object}
183
+ * @param path {string[]}
184
+ * @param createVal {*} If set, non-existent paths will be created and value at path will be set to createVal.
185
+ * @return {*} The value, or undefined if it can't be reached. */
186
+ function delve(obj, path, createVal = d) {
187
+ let isCreate = createVal !== d;
356
188
 
357
- /**
358
- * Used by NodeGroup.applyComponentExprs() */
359
- componentHash: new WeakMap(),
189
+ let len = path.length;
190
+ if (!obj && !isCreate && len)
191
+ return undefined;
360
192
 
361
- /**
362
- * Store which instances of Solarite have already been added to the DOM.
363
- * @type {WeakSet<HTMLElement>} */
364
- connected: new WeakSet(),
193
+ let i = 0;
194
+ for (let srcProp of path) {
365
195
 
366
- /**
367
- * Elements that have been rendered to by r() at least once.
368
- * This is used by the Solarite class to know when to call onFirstConnect()
369
- * @type {WeakSet<HTMLElement>} */
370
- rendered: new WeakSet(),
196
+ // If the path is undefined and we're not to the end yet:
197
+ if (obj[srcProp] === undefined) {
371
198
 
372
- /**
373
- * Used by watch3 to see which expressions are being accessed.
374
- * @type {[]}*/
375
- currentExprPath: null,
199
+ // If the next index is an integer or integer string.
200
+ if (isCreate) {
201
+ if (i < len - 1) {
202
+ // If next level path is a number, create as an array
203
+ let isArray = (path[i + 1] + '').match(/^\d+$/);
204
+ obj[srcProp] = isArray ? [] : {};
205
+ }
206
+ } else
207
+ return undefined; // can't traverse
208
+ }
376
209
 
377
- /**
378
- * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
379
- elementClasses: {},
210
+ // If last item in path
211
+ if (isCreate && i === len - 1)
212
+ obj[srcProp] = createVal;
380
213
 
381
- /**
382
- * Used by ExprPath.applyEventAttrib()
383
- * @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
384
- nodeEvents: new WeakMap(),
214
+ // Traverse deeper along destination object.
215
+ obj = obj[srcProp];
216
+ i++;
217
+ }
385
218
 
386
- /**
387
- * Get the RootNodeGroup for an element.
388
- * @type {WeakMap<HTMLElement, RootNodeGroup>} */
389
- nodeGroups: new WeakMap(),
219
+ return obj;
220
+ }
221
+
222
+ // d means "don't create"
223
+ let d = {};
224
+
225
+ let Util = {
390
226
 
391
227
  /**
392
- * Used by r() path 9. */
393
- objToEl: new WeakMap(),
228
+ * Returns true if they're the same.
229
+ * @param a
230
+ * @param b
231
+ * @returns {boolean} */
232
+ arraySame(a, b) {
233
+ let aLength = a.length;
234
+ if (aLength !== b.length)
235
+ return false;
236
+ for (let i=0; i<aLength; i++)
237
+ if (a[i] !== b[i])
238
+ return false;
239
+ return true; // the same.
240
+ },
394
241
 
395
- pendingChildren: [],
242
+ bindId(root, el) {
243
+ let id = el.getAttribute('data-id') || el.getAttribute('id');
244
+ if (id) { // If something hasn't removed the id.
396
245
 
397
- /**
398
- * Elements that are currently rendering via the r() function.
399
- * @type {WeakSet<HTMLElement>} */
400
- rendering: new WeakSet(),
246
+ // Don't allow overwriting existing class properties if they already have a non-Node value.
247
+ if (root[id] && !(root[id] instanceof Node))
248
+ throw new Error(`${root.constructor.name}.${id} already has a value. ` +
249
+ `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
401
250
 
402
- /**
403
- * Map from array of Html strings to a Shell created from them.
404
- * @type {WeakMap<string[], Shell>} */
405
- shells: new WeakMap()
406
- };
407
-
408
- let Util = {
251
+ delve(root, id.split(/\./g), el);
252
+ }
253
+ },
409
254
 
255
+ /**
256
+ * @param style {HTMLStyleElement}
257
+ * @param root {HTMLElement} */
410
258
  bindStyles(style, root) {
411
259
  let styleId = root.getAttribute('data-style');
412
260
  if (!styleId) {
@@ -419,17 +267,59 @@ let Util = {
419
267
  root.setAttribute('data-style', styleId);
420
268
  }
421
269
 
270
+ // Replace ":host" with "tagName[data-style=...]" in the css.
422
271
  let tagName = root.tagName.toLowerCase();
423
272
  for (let child of style.childNodes) {
424
273
  if (child.nodeType === 3) {
425
274
  let oldText = child.textContent;
426
- let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, tagName + '[data-style="' + styleId + '"]');
275
+ let newText = oldText.replace(/:host(?=[^a-z0-9_])/gi, `${tagName}[data-style="${styleId}"]`);
427
276
  if (oldText !== newText)
428
277
  child.textContent = newText;
429
278
  }
430
279
  }
431
280
  },
432
281
 
282
+ /**
283
+ * Convert a Proper Case name to a name with dashes.
284
+ * Dashes will be placed between letters and numbers.
285
+ * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
286
+ * @param str {string}
287
+ * @return {string}
288
+ *
289
+ * @example
290
+ * 'ProperName' => 'proper-name'
291
+ * 'HTMLElement' => 'html-element'
292
+ * 'BigUI' => 'big-ui'
293
+ * 'UIForm' => 'ui-form'
294
+ * 'A100' => 'a-100' */
295
+ camelToDashes(str) {
296
+ // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
297
+ str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
298
+
299
+ // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
300
+ str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
301
+
302
+ // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
303
+ str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
304
+
305
+ // Convert all the remaining capital letters to lowercase.
306
+ return str.toLowerCase();
307
+ },
308
+
309
+ /**
310
+ * Converts a string written in kebab-case to camelCase.
311
+ *
312
+ * @param {string} str - The input string written in kebab-case.
313
+ * @return {string} - The resulting camelCase string.
314
+ *
315
+ * @example
316
+ * dashesToCamel('example-string') // Returns 'exampleString'
317
+ * dashesToCamel('another-example-test') // Returns 'anotherExampleTest' */
318
+ dashesToCamel(str) {
319
+ return str.replace(/-([a-z])/g, g => g[1].toUpperCase());
320
+ },
321
+
322
+
433
323
  /**
434
324
  * A generator function that recursively traverses and flattens a value.
435
325
  *
@@ -456,23 +346,24 @@ let Util = {
456
346
  * for (const item of flatten(complexArray))
457
347
  * console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
458
348
  */
459
- *flatten(value) {
460
- if (Array.isArray(value)) {
461
- for (const item of value) {
462
- yield* Util.flatten(item); // Recursively flatten arrays
463
- }
464
- } else if (typeof value === 'function') {
465
- const result = value();
466
- yield* Util.flatten(result); // Recursively flatten the result of a function
467
- } else
468
- yield value; // Yield primitive values as is
469
- },
349
+ // *flatten(value) {
350
+ // if (Array.isArray(value)) {
351
+ // for (const item of value) {
352
+ // yield* Util.flatten(item); // Recursively flatten arrays
353
+ // }
354
+ // } else if (typeof value === 'function') {
355
+ // const result = value();
356
+ // yield* Util.flatten(result); // Recursively flatten the result of a function
357
+ // } else
358
+ // yield value; // Yield primitive values as is
359
+ // },
470
360
 
471
361
  /**
472
362
  * Get the value of an input as the most appropriate JavaScript type.
473
363
  * @param node {HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement|HTMLDivElement}
474
364
  * @return {string|string[]|number|[]|File[]|Date|boolean} */
475
365
  getInputValue(node) {
366
+ // .type is a built-in DOM property
476
367
  if (node.type === 'checkbox' || node.type === 'radio')
477
368
  return node.checked; // Boolean
478
369
  if (node.type === 'file')
@@ -487,48 +378,84 @@ let Util = {
487
378
  return node.value; // String
488
379
  },
489
380
 
381
+ isEvent(attrName) {
382
+ return attrName.startsWith('on') && attrName in Globals$1.div;
383
+ },
384
+
385
+ /**
386
+ * @param el {HTMLElement}
387
+ * @param prop {string}
388
+ * @returns {boolean} */
389
+ isHtmlProp(el, prop) {
390
+ let key = el.tagName + '.' + prop;
391
+ let result = Globals$1.htmlProps[key];
392
+ if (result === undefined) { // Caching just barely makes this slightly faster.
393
+ let proto = Object.getPrototypeOf(el);
394
+
395
+ // Find the first HTMLElement that we inherit from (not our own classes)
396
+ while (proto) {
397
+ const ctorName = proto.constructor.name;
398
+ if (ctorName.startsWith('HTML') && ctorName.endsWith('Element'))
399
+ break
400
+ proto = Object.getPrototypeOf(proto);
401
+ }
402
+ Globals$1.htmlProps[key] = result = (proto
403
+ ? !!Object.getOwnPropertyDescriptor(proto, prop)?.set
404
+ : false);
405
+ }
406
+ return result;
407
+ },
408
+
490
409
  /**
491
410
  * Is it an array and a path that can be evaluated by delve() ?
411
+ * We allow the first element to be null/undefined so binding can report errors.
492
412
  * @param arr {Array|*}
493
413
  * @returns {boolean} */
494
414
  isPath(arr) {
495
- return Array.isArray(arr) && typeof arr[0] === 'object' && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number');
415
+ return Array.isArray(arr) && arr.length >=2 // An array of at least two elements.
416
+ && (typeof arr[0] === 'object' || arr[0] === undefined) // Where the first element is an object, null, or undefined.
417
+ && !arr.slice(1).find(p => typeof p !== 'string' && typeof p !== 'number'); // Path 1..x is only numbers and strings.
418
+ },
419
+
420
+ isFalsy(val) {
421
+ return val === undefined || val === false || val === null;
422
+ },
423
+
424
+ /*
425
+ isPrimitive(val) {
426
+ return typeof val === 'string' || typeof val === 'number'
427
+ },*/
428
+
429
+ /**
430
+ * If val is a function, evaluate it recursively until the result is not a function.
431
+ * If it's an array or an object, convert it to Json.
432
+ * If it's a Date, format it as Y-m-d H:i:s
433
+ * @param val
434
+ * @returns {string|number|boolean} */
435
+ makePrimitive(val) {
436
+ if (typeof val === 'function')
437
+ return Util.makePrimitive(val());
438
+ else if (val instanceof Date)
439
+ return val.toISOString().replace(/T/, ' ');
440
+ else if (Array.isArray(val) || typeof val === 'object')
441
+ return ''; // JSON.stringify(val);
442
+ return val;
496
443
  },
497
444
 
498
445
  /**
499
- * Find NodeGroups that had their nodes removed and add those nodes to a Fragment so
500
- * they're not lost forever and the NodeGroup's internal structure is still consistent.
501
- * This saves all of a NodeGroup's nodes in order, so that nextChildNode still works.
502
- * This is necessary because a NodeGroup normally only stores the first and last node.
503
- * Called from ExprPath.apply().
504
- * @param oldNodeGroups {NodeGroup[]}
505
- * @param oldNodes {Node[]} */
506
- saveOrphans(oldNodeGroups, oldNodes) {
507
- let oldNgMap = new Map();
508
- for (let ng of oldNodeGroups) {
509
- oldNgMap.set(ng.startNode, ng);
510
-
511
- // TODO: Is this necessary?
512
- // if (ng.parentPath)
513
- // ng.parentPath.clearNodesCache();
514
- }
515
-
516
- for (let i=0, node; node = oldNodes[i]; i++) {
517
- let ng;
518
- if (!node.parentNode && (ng = oldNgMap.get(node))) {
519
- //ng.nodesCache = [];
520
- let fragment = document.createDocumentFragment();
521
- let endNode = ng.endNode;
522
- while (node !== endNode) {
523
- fragment.append(node);
524
- //ng.nodesCache.push(node);
525
- i++;
526
- node = oldNodes[i];
527
- }
528
- fragment.append(endNode);
529
- //ng.nodesCache.push(endNode);
530
- }
446
+ * Use an array as the value of a map, appending to it when we add.
447
+ * Used by watch.js.
448
+ * @param map {Map|WeakMap|Object}
449
+ * @param key
450
+ * @param value */
451
+ mapArrayAdd(map, key, value) {
452
+ let result = map.get(key);
453
+ if (!result) {
454
+ result = [value];
455
+ map.set(key, result);
531
456
  }
457
+ else
458
+ result.push(value);
532
459
  },
533
460
 
534
461
  /**
@@ -559,134 +486,6 @@ let Util = {
559
486
 
560
487
 
561
488
 
562
- let div = document.createElement('div');
563
-
564
- let isEvent = attrName => attrName.startsWith('on') && attrName in div;
565
-
566
-
567
- /**
568
- * Convert a Proper Case name to a name with dashes.
569
- * Dashes will be placed between letters and numbers.
570
- * If there are multiple consecutive capital letters followed by another chracater, a dash will be placed before the last capital letter.
571
- * @param str {string}
572
- * @return {string}
573
- *
574
- * @example
575
- * 'ProperName' => 'proper-name'
576
- * 'HTMLElement' => 'html-element'
577
- * 'BigUI' => 'big-ui'
578
- * 'UIForm' => 'ui-form'
579
- * 'A100' => 'a-100' */
580
- function camelToDashes(str) {
581
- // Convert any capital letter that is preceded by a lowercase letter or number to lowercase and precede with a dash.
582
- str = str.replace(/([a-z0-9])([A-Z])/g, '$1-$2');
583
-
584
- // Convert any capital letter that is followed by a lowercase letter or number to lowercase and precede with a dash.
585
- str = str.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2');
586
-
587
- // Convert any number that is preceded by a lowercase or uppercase letter to be preceded by a dash.
588
- str = str.replace(/([a-zA-Z])([0-9])/g, '$1-$2');
589
-
590
- // Convert all the remaining capital letters to lowercase.
591
- return str.toLowerCase();
592
- }
593
-
594
-
595
-
596
-
597
-
598
- /**
599
- * Returns false if they're the same. Or the first index where they differ.
600
- * @param a
601
- * @param b
602
- * @returns {boolean} */
603
- function arraySame(a, b) {
604
- let aLength = a.length;
605
- if (aLength !== b.length)
606
- return false;
607
- for (let i=0; i<aLength; i++)
608
- if (a[i] !== b[i])
609
- return false;
610
- return true; // the same.
611
- }
612
-
613
-
614
- /**
615
- * TODO: Turn this into a class because it has internal state.
616
- * TODO: Don't break on 3<a inside a <script> or <style> tag.
617
- * @param html {?string} Pass null to reset context.
618
- * @returns {string} */
619
- function htmlContext(html) {
620
- if (html === null) {
621
- state = {...defaultState};
622
- return state.context;
623
- }
624
- for (let i = 0; i < html.length; i++) {
625
- const char = html[i];
626
- switch (state.context) {
627
- case htmlContext.Text:
628
- if (char === '<' && html[i+1].match(/[a-z!]/i)) { // Start of a tag or comment.
629
- // if (html.slice(i, i+4) === '<!--')
630
- // state.context = htmlContext.Comment;
631
- // else
632
- state.context = htmlContext.Tag;
633
- state.buffer = '';
634
- }
635
- break;
636
- case htmlContext.Tag:
637
- if (char === '>') {
638
- state.context = htmlContext.Text;
639
- state.quote = null;
640
- state.buffer = '';
641
- } else if (char === ' ' && !state.buffer) {
642
- // No attribute name is present. Skipping the space.
643
- continue;
644
- } else if (char === ' ' || char === '/' || char === '?') {
645
- state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
646
- } else if (char === '"' || char === "'" || char === '=') {
647
- state.context = htmlContext.Attribute;
648
- state.quote = char === '=' ? null : char;
649
- state.buffer = '';
650
- } else {
651
- state.buffer += char;
652
- }
653
- break;
654
- case htmlContext.Attribute:
655
- if (!state.quote && !state.buffer.length && (char === '"' || char === "'"))
656
- state.quote = char;
657
-
658
- else if (char === state.quote || (!state.quote && state.buffer.length)) {
659
- state.context = htmlContext.Tag;
660
- state.quote = null;
661
- state.buffer = '';
662
- } else if (!state.quote && char === '>') {
663
- state.context = htmlContext.Text;
664
- state.quote = null;
665
- state.buffer = '';
666
- } else if (char !== ' ') {
667
- state.buffer += char;
668
- }
669
- break;
670
- }
671
-
672
- }
673
- return state.context;
674
- }
675
-
676
-
677
- htmlContext.Attribute = 'Attribute';
678
- htmlContext.Text = 'Text';
679
- htmlContext.Tag = 'Tag';
680
- //htmlContext.Comment = 'Comment';
681
- let defaultState = {
682
- context: htmlContext.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
683
- quote: null, // possible values: null, '"', "'"
684
- buffer: '',
685
- lastChar: null
686
- };
687
- let state = {...defaultState};
688
-
689
-
690
489
  // For debugging only
691
490
  //#IFDEV
692
491
  function setIndent(items, level=1) {
@@ -751,7 +550,7 @@ function flattenAndIndent(inputArray, indent = "") {
751
550
 
752
551
  class MultiValueMap {
753
552
 
754
- /** @type {Object<string, Set>} */
553
+ /** @type {Record<string, Set>} */
755
554
  data = {};
756
555
 
757
556
  // Set a new value for a key
@@ -785,23 +584,14 @@ class MultiValueMap {
785
584
  * @param val If specified, make sure we delete this specific value, if a key exists more than once.
786
585
  * @returns {*|undefined} The deleted item. */
787
586
  delete(key, val=undefined) {
788
- // if (key === '["Html2",[[["Html3",["F1","A"]],["Html3",["F1","B"]]]]]')
789
- // debugger;
790
-
791
587
  let data = this.data;
792
-
793
- // if (!data.hasOwnProperty(key))
794
- // return undefined;
795
-
796
- // Delete a specific value.
797
588
  let result;
798
589
  let set = data[key];
799
- if (!set) // slower than pre-check.
590
+ if (!set)
800
591
  return undefined;
801
592
 
802
593
  // Delete any value.
803
594
  if (val === undefined) {
804
- //result = set.values().next().value; // get first item from set.
805
595
  [result] = set; // Does the same as above and seems to be about the same speed.
806
596
  set.delete(result);
807
597
  }
@@ -812,7 +602,6 @@ class MultiValueMap {
812
602
  result = val;
813
603
  }
814
604
 
815
- // TODO: Will this make it slower?
816
605
  if (set.size === 0)
817
606
  delete data[key];
818
607
 
@@ -820,28 +609,34 @@ class MultiValueMap {
820
609
  }
821
610
 
822
611
  /**
823
- * Try to delete an item that matches the key and the isPreferred function.
824
- * if not the latter, just delete any item that matches the key.
612
+ * Remove one value from a key, and return it.
825
613
  * @param key {string}
826
- * @param isPreferred {function}
827
614
  * @returns {*|undefined} The deleted item. */
828
- deletePreferred(key, isPreferred) {
615
+ deleteAny(key) {
616
+ let data = this.data;
829
617
  let result;
618
+ let set = data[key];
619
+ if (!set) // slower than pre-check.
620
+ return undefined;
621
+
622
+ [result] = set; // Does the same as above and seems to be about the same speed.
623
+ set.delete(result);
624
+
625
+ if (set.size === 0)
626
+ delete data[key];
627
+
628
+ return result;
629
+ }
630
+
631
+ deleteSpecific(key, val) {
830
632
  let data = this.data;
633
+ let result;
831
634
  let set = data[key];
832
635
  if (!set)
833
636
  return undefined;
834
637
 
835
- for (let val of set)
836
- if (isPreferred(val)) {
837
- set.delete(val);
838
- result = val;
839
- break;
840
- }
841
- if (!result) {
842
- [result] = set;
843
- set.delete(result);
844
- }
638
+ set.delete(val);
639
+ result = val;
845
640
 
846
641
  if (set.size === 0)
847
642
  delete data[key];
@@ -1089,14 +884,23 @@ const udomdiff = (parentNode, a, b, before) => {
1089
884
  return b;
1090
885
  };
1091
886
 
887
+ //import {ArraySpliceOp} from "./watch.js";
888
+ //#IFDEV
889
+ var exprPathId = 0;
890
+ //#ENDIF
891
+
1092
892
  /**
1093
893
  * Path to where an expression should be evaluated within a Shell or NodeGroup.
1094
894
  * Path is only valid until the expressions before it are evaluated.
1095
895
  * TODO: Make this based on parent and node instead of path? */
1096
896
  class ExprPath {
1097
897
 
898
+ //#IFDEV
899
+ eid = exprPathId++;
900
+ //#ENDIF
901
+
1098
902
  /**
1099
- * @type {PathType} */
903
+ * @type {ExprPathType} */
1100
904
  type;
1101
905
 
1102
906
  // Used for attributes:
@@ -1112,8 +916,6 @@ class ExprPath {
1112
916
  * @type {Set<string>} Used for type=AttribType.Multiple to remember the attributes that were added. */
1113
917
  attrNames;
1114
918
 
1115
-
1116
-
1117
919
  /**
1118
920
  * @type {Node} Node that occurs before this ExprPath's first Node.
1119
921
  * This is necessary because udomdiff() can steal nodes from another ExprPath.
@@ -1155,16 +957,23 @@ class ExprPath {
1155
957
  nodeMarkerPath;
1156
958
 
1157
959
 
1158
- /** @type {?function} */
960
+ /** @type {?function} A function called by renderWatched() to update the value of this expression. */
1159
961
  watchFunction
1160
962
 
963
+ /**
964
+ * @type {?function} The most recent callback passed to a .map() function in this ExprPath.
965
+ * TODO: What if one ExprPath has two .map() calls? Maybe we just won't support that. */
966
+ mapCallback
967
+
968
+ isHtmlProperty = undefined;
969
+
1161
970
  /**
1162
971
  * @param nodeBefore {Node}
1163
972
  * @param nodeMarker {?Node}
1164
- * @param type {PathType}
973
+ * @param type {ExprPathType}
1165
974
  * @param attrName {?string}
1166
975
  * @param attrValue {string[]} */
1167
- constructor(nodeBefore, nodeMarker, type=PathType.Content, attrName=null, attrValue=null) {
976
+ constructor(nodeBefore, nodeMarker, type=ExprPathType.Content, attrName=null, attrValue=null) {
1168
977
 
1169
978
  // If path is a node.
1170
979
  this.nodeBefore = nodeBefore;
@@ -1172,7 +981,7 @@ class ExprPath {
1172
981
  this.type = type;
1173
982
  this.attrName = attrName;
1174
983
  this.attrValue = attrValue;
1175
- if (type === PathType.Multiple)
984
+ if (type === ExprPathType.AttribMultiple)
1176
985
  this.attrNames = new Set();
1177
986
  }
1178
987
 
@@ -1186,36 +995,27 @@ class ExprPath {
1186
995
  * We should modify path.applyValueAttrib so it stores the procssed parts and then only calls
1187
996
  * setAttribute() once all the pieces are in place.
1188
997
  *
1189
- * @param expr {Expr}
1190
998
  * @param exprs {Expr[]}
1191
- * @param exprIndex {int}
1192
- * @param componentExprs {object}
1193
- * @returns {int} */
1194
- apply(expr, exprs=null, exprIndex=0, componentExprs={}) {
999
+ * @param freeNodeGroups {boolean} */
1000
+ apply(exprs, freeNodeGroups=true) {
1195
1001
  switch (this.type) {
1196
1002
  case 1: // PathType.Content:
1197
- this.applyNodes(expr);
1003
+ this.applyNodes(exprs[0], freeNodeGroups);
1198
1004
  break;
1199
1005
  case 2: // PathType.Multiple:
1200
- this.applyMultipleAttribs(this.nodeMarker, expr);
1006
+ this.applyMultipleAttribs(this.nodeMarker, exprs[0]);
1201
1007
  break;
1202
1008
  case 5: // PathType.Comment:
1203
1009
  // Expressions inside Html comments. Deliberately empty because we won't waste time updating them.
1204
1010
  break;
1205
1011
  case 6: // PathType.Event:
1206
- this.applyEventAttrib(this.nodeMarker, expr, this.parentNg.rootNg.root);
1012
+ this.applyEventAttrib(this.nodeMarker, exprs[0], this.parentNg.rootNg.root);
1207
1013
  break;
1208
- default:
1209
- if (this.type === 4 /*PathType.Component*/ && this.nodeMarker !== this.parentNg.rootNg.root)
1210
- componentExprs[this.attrName] = expr;
1211
- else {
1212
- // One attribute value may have multiple expressions. Here we apply them all at once.
1213
- exprIndex = this.applyValueAttrib(this.nodeMarker, exprs || [expr], exprIndex);
1214
- }
1014
+ default: // TODO: Is this still used? Lots of tests fail without it.
1015
+ // One attribute value may have multiple expressions. Here we apply them all at once.
1016
+ this.applyValueAttrib(this.nodeMarker, exprs);
1215
1017
  break;
1216
1018
  }
1217
-
1218
- return exprIndex;
1219
1019
  }
1220
1020
 
1221
1021
  /**
@@ -1223,14 +1023,16 @@ class ExprPath {
1223
1023
  * Called by applyExprs()
1224
1024
  * This function is recursive, as the functions it calls also call it.
1225
1025
  * @param expr {Expr}
1026
+ * @param freeNodeGroups {boolean}
1226
1027
  * @return {Node[]} New Nodes created. */
1227
- applyNodes(expr) {
1028
+ applyNodes(expr, freeNodeGroups=true) {
1228
1029
  let path = this;
1229
1030
 
1230
1031
  // This can be done at the beginning or the end of this function.
1231
1032
  // If at the end, we may get rendering done faster.
1232
1033
  // But when at the beginning, it leaves all the nodes in-use so we can do a renderWatched().
1233
- path.freeNodeGroups();
1034
+ if (freeNodeGroups)
1035
+ path.freeNodeGroups();
1234
1036
 
1235
1037
  /*#IFDEV*/path.verify();/*#ENDIF*/
1236
1038
 
@@ -1240,10 +1042,10 @@ class ExprPath {
1240
1042
  /*#IFDEV*/assert(!oldNodeGroups.includes(null));/*#ENDIF*/
1241
1043
  let secondPass = []; // indices
1242
1044
 
1243
- path.nodeGroups = []; // Reset before applyExact and the code below rebuilds it.
1244
- path.applyExact(expr, newNodes, secondPass);
1045
+ path.nodeGroups = []; // Reset before applyExactNodes and the code below rebuilds it.
1046
+ path.applyExactNodes(expr, newNodes, secondPass);
1245
1047
 
1246
- this.existingTextNodes = null;
1048
+ //this.existingTextNodes = null;
1247
1049
 
1248
1050
  // TODO: Create an array of old vs Nodes and NodeGroups together.
1249
1051
  // If they're all the same, skip the next steps.
@@ -1281,7 +1083,7 @@ class ExprPath {
1281
1083
 
1282
1084
 
1283
1085
  // This pre-check makes it a few percent faster?
1284
- let same = arraySame(oldNodes, newNodes);
1086
+ let same = Util.arraySame(oldNodes, newNodes);
1285
1087
  if (!same) {
1286
1088
 
1287
1089
  path.nodesCache = newNodes; // Replaces value set by path.getNodes()
@@ -1302,7 +1104,21 @@ class ExprPath {
1302
1104
 
1303
1105
  for (let ng of oldNodeGroups)
1304
1106
  if (!ng.startNode.parentNode)
1305
- ng.saveOrphans();
1107
+ ng.removeAndSaveOrphans();
1108
+
1109
+
1110
+
1111
+
1112
+ // Instantiate components created within ${...} expressions.
1113
+ // Embedded style tags are handled elsewhere, but where?
1114
+ for (let el of newNodes) {
1115
+ if (el instanceof HTMLElement) {
1116
+ if (el.hasAttribute('solarite-placeholder'))
1117
+ this.parentNg.instantiateComponent(el);
1118
+ for (let child of el.querySelectorAll('[solarite-placeholder]'))
1119
+ this.parentNg.instantiateComponent(child);
1120
+ }
1121
+ }
1306
1122
  }
1307
1123
 
1308
1124
 
@@ -1310,49 +1126,145 @@ class ExprPath {
1310
1126
  }
1311
1127
 
1312
1128
  /**
1313
- * Used by watch() for replacing individual loop items. */
1314
- applyLoopItemUpdate(index, template) {
1315
- // At this point none of the nodes being used will be in nodeGroupsFree.
1316
- let oldNg = this.nodeGroups[index];
1317
- this.nodeGroupsFree.add(oldNg.exactKey, oldNg);
1318
- this.nodeGroupsFree.add(oldNg.closeKey, oldNg);
1129
+ * Used by watch() for inserting/removing/replacing individual loop items.
1130
+ * @param op {ArraySpliceOp} */
1131
+ applyArrayOp(op) {
1319
1132
 
1320
- let ng = this.getNodeGroup(template, true);
1321
- if (ng) {
1322
- return; // It's an exactl match, so replace nothing.
1323
- }
1133
+ // Replace NodeGroups
1134
+ let replaceCount = Math.min(op.deleteCount, op.items.length);
1135
+ let deleteCount = op.deleteCount - replaceCount;
1136
+ for (let i=0; i<replaceCount; i++) {
1137
+ let oldNg = this.nodeGroups[op.index + i]; // TODO: One expr can create multiple nodegroups.
1324
1138
 
1139
+ // Try to find an exact match
1140
+ let func = this.mapCallback || this.watchFunction;
1141
+ let expr = func(op.items[i]);
1325
1142
 
1143
+ // If the result of func isn't a template, conver it to one or more templates.
1144
+ this.exprToTemplates(expr, template => { // TODO: An expr can create multiple NodeGroups. I need a way to group them.
1326
1145
 
1327
- ng = this.getNodeGroup(template, false);
1146
+ let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1147
+ if (ng && ng === oldNg) ; else {
1328
1148
 
1329
- this.nodeGroups[index] = ng;
1149
+ // Find a close match or create a new node group
1150
+ if (!ng)
1151
+ ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1152
+ this.nodeGroups[op.index + i] = ng; // TODO: Remove old one to nodeGroupsDetached?
1330
1153
 
1331
- // Splice in the new nodes.
1332
- for (let node of ng.getNodes()) {
1333
- oldNg.startNode.parentNode.insertBefore(node, oldNg.startNode);
1154
+ // Splice in the new nodes.
1155
+ let insertBefore = oldNg.startNode;
1156
+ for (let node of ng.getNodes())
1157
+ insertBefore.parentNode.insertBefore(node, insertBefore);
1158
+
1159
+ // Remove the old nodes.
1160
+ if (ng !== oldNg)
1161
+ oldNg.removeAndSaveOrphans();
1162
+ }
1163
+ });
1164
+ }
1165
+
1166
+ // Delete extra at the end.
1167
+ if (deleteCount > 0) {
1168
+ for (let i=0; i<deleteCount; i++) {
1169
+ let oldNg = this.nodeGroups[op.index + replaceCount + i];
1170
+ oldNg.removeAndSaveOrphans();
1171
+ }
1172
+ this.nodeGroups.splice(op.index + replaceCount, deleteCount);
1334
1173
  }
1335
1174
 
1336
- if (oldNg !== ng) {
1337
- for (let node of oldNg.getNodes())
1338
- node.remove();
1339
- oldNg.saveOrphans();
1175
+ // Add extra at the end.
1176
+ else {
1177
+ let newItems = op.items.slice(replaceCount);
1178
+
1179
+ let insertBefore = this.nodeGroups[op.index + replaceCount]?.startNode || this.nodeMarker;
1180
+ for (let i = 0; i < newItems.length; i++) { // We use nodeMarker if the subequent (or all) nodeGroups have been removed.
1181
+
1182
+
1183
+ // Try to find exact match
1184
+ let template = this.mapCallback(newItems[i]);
1185
+ let ng = this.getNodeGroup(template, true); // Removes from nodeGroupsAttached and adds to nodeGroupsRendered()
1186
+ if (!ng) // Find a close match or create a new node group
1187
+ ng = this.getNodeGroup(template, false); // adds back to nodeGroupsRendered()
1188
+
1189
+ this.nodeGroups.push(ng);
1190
+
1191
+ // Splice in the new nodes.
1192
+ for (let node of ng.getNodes())
1193
+ insertBefore.parentNode.insertBefore(node, insertBefore);
1194
+ }
1340
1195
  }
1341
1196
 
1197
+ //#IFDEV
1198
+ assert(this.nodeGroups.length === op.array.length);
1199
+ //#ENDIF
1200
+
1342
1201
  // TODO: update or invalidate the nodes cache?
1343
1202
  this.nodesCache = null;
1344
1203
  }
1345
1204
 
1205
+ /**
1206
+ * Recursively traverse expr.
1207
+ * If a value is a function, evaluate it.
1208
+ * If a value is an array, recurse on each item.
1209
+ * If it's a primitive, convert it to a Template.
1210
+ * Otherwise pass the item (which is now either a Template or a Node) to callback.
1211
+ * @param expr
1212
+ * @param callback {function(Node|Template)}
1213
+ *
1214
+ * TODO: have applyExactNodes() use this function. */
1215
+ exprToTemplates(expr, callback) {
1216
+ if (Array.isArray(expr))
1217
+ for (let subExpr of expr)
1218
+ this.exprToTemplates(subExpr, callback);
1219
+
1220
+ else if (typeof expr === 'function') {
1221
+ // TODO: One ExprPath can have multiple expr functions.
1222
+ // But if using it as a watch, it should only have one at the top level.
1223
+ // So maybe this is ok.
1224
+ Globals$1.currentExprPath = this; // Used by watch()
1225
+
1226
+ this.watchFunction = expr; // TODO: Only do this if it's a top level function.
1227
+ expr = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
1228
+ Globals$1.currentExprPath = null;
1229
+
1230
+ this.exprToTemplates(expr, callback);
1231
+ }
1232
+
1233
+ // String/Number/Date/Boolean
1234
+ else if (!(expr instanceof Template) && !(expr instanceof Node)){
1235
+ // Convert expression to a string.
1236
+ if (expr === undefined || expr === false || expr === null) // Util.isFalsy() inlined
1237
+ expr = '';
1238
+ else if (typeof expr !== 'string')
1239
+ expr += '';
1240
+
1241
+ // Get the same Template for the same string each time.
1242
+ // let template = Globals.stringTemplates[expr];
1243
+ // if (!template) {
1244
+ let template = new Template([expr], []);
1245
+ // Globals.stringTemplates[expr] = template;
1246
+ //}
1247
+
1248
+ // Recurse.
1249
+ this.exprToTemplates(template, callback);
1250
+ }
1251
+ else
1252
+ callback(expr);
1253
+ }
1254
+
1346
1255
 
1347
1256
  /**
1348
- * Apply Nodes that are an exact match.
1257
+ * Try to apply Nodes that are an exact match, by finding existing nodes from the last render
1258
+ * that have the same value as created by the expr.
1259
+ * This is called from ExprPath.applyNodes().
1260
+ *
1349
1261
  * @param expr {Template|Node|Array|function|*}
1350
- * @param newNodes {(Node|Template)[]}
1351
- * @param secondPass {Array} Locations within newNodes to evaluate later. */
1352
- applyExact(expr, newNodes, secondPass) {
1262
+ * @param newNodes {(Node|Template)[]} An inout parameter; we add the nodes here as we go.
1263
+ * @param secondPass {[int, int][]} Locations within newNodes for ExprPath.applyNodes() to evaluate later,
1264
+ * when it tries to find partial matches. */
1265
+ applyExactNodes(expr, newNodes, secondPass) {
1353
1266
 
1354
1267
  if (expr instanceof Template) {
1355
-
1356
1268
  let ng = this.getNodeGroup(expr, true);
1357
1269
  if (ng) {
1358
1270
 
@@ -1370,7 +1282,7 @@ class ExprPath {
1370
1282
  }
1371
1283
  }
1372
1284
 
1373
- // Node created by an expression.
1285
+ // Node(s) created by an expression.
1374
1286
  else if (expr instanceof Node) {
1375
1287
 
1376
1288
  // DocumentFragment created by an expression.
@@ -1383,53 +1295,14 @@ class ExprPath {
1383
1295
  // Arrays and functions.
1384
1296
  // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
1385
1297
  // but that consistently made the js-framework-benchmarks a few percentage points slower.
1386
- else if (Array.isArray(expr))
1387
- for (let subExpr of expr)
1388
- this.applyExact(subExpr, newNodes, secondPass);
1389
-
1390
- else if (typeof expr === 'function') {
1391
- // TODO: One ExprPath can have multiple expr functions.
1392
- // But if using it as a watch, it should only have one at the top level.
1393
- // So maybe this is ok.
1394
- Globals.currentExprPath = [this, expr]; // Used by watch3()
1395
- this.watchFunction = expr; // TODO: Only do this if it's a top level function.
1396
- let result = expr();
1397
- Globals.currentExprPath = null;
1398
-
1399
- this.applyExact(result, newNodes, secondPass);
1400
- }
1401
-
1402
- // Text
1403
- else {
1404
- // Convert falsy values (but not 0) to empty string.
1405
- // Convert numbers to string so they compare the same.
1406
- let text = (expr === undefined || expr === false || expr === null) ? '' : (expr + '');
1407
-
1408
- // Fast path for updating the text of a single text node.
1409
- let first = this.nodeBefore.nextSibling;
1410
- if (first.nodeType === 3 && first.nextSibling === this.nodeMarker && !newNodes.includes(first)) {
1411
- if (first.textContent !== text)
1412
- first.textContent = text;
1413
-
1414
- newNodes.push(first);
1415
- }
1416
-
1417
- else {
1418
- // TODO: Optimize this into a Set or Map or something?
1419
- if (!this.existingTextNodes)
1420
- this.existingTextNodes = this.getNodes().filter(n => n.nodeType === 3);
1421
-
1422
- let idx = this.existingTextNodes.findIndex(n => n.textContent === text);
1423
- if (idx !== -1)
1424
- newNodes.push(...this.existingTextNodes.splice(idx, 1));
1425
- else
1426
- newNodes.push(this.nodeMarker.ownerDocument.createTextNode(text));
1427
- }
1428
- }
1298
+ else
1299
+ this.exprToTemplates(expr, template => {
1300
+ this.applyExactNodes(template, newNodes, secondPass);
1301
+ });
1429
1302
  }
1430
1303
 
1431
1304
  applyMultipleAttribs(node, expr) {
1432
- /*#IFDEV*/assert(this.type === PathType.Multiple);/*#ENDIF*/
1305
+ /*#IFDEV*/assert(this.type === ExprPathType.AttribMultiple);/*#ENDIF*/
1433
1306
 
1434
1307
  if (Array.isArray(expr))
1435
1308
  expr = expr.flat().join(' '); // flat and join so we can accept arrays of arrays of strings.
@@ -1438,6 +1311,13 @@ class ExprPath {
1438
1311
  let oldNames = this.attrNames;
1439
1312
  this.attrNames = new Set();
1440
1313
  if (expr) {
1314
+ if (typeof expr === 'function') {
1315
+ Globals$1.currentExprPath = this; // Used by watch()
1316
+ this.watchFunction = expr; // used by renderWatched()
1317
+ expr = expr();
1318
+ Globals$1.currentExprPath = null;
1319
+ }
1320
+
1441
1321
  let attrs = (expr +'') // Split string into multiple attributes.
1442
1322
  .split(/([\w-]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s]+))/g)
1443
1323
  .map(text => text.trim())
@@ -1468,52 +1348,57 @@ class ExprPath {
1468
1348
  * @param root */
1469
1349
  applyEventAttrib(node, expr, root) {
1470
1350
  /*#IFDEV*/
1471
- assert(this.type === PathType.Event/* || this.type === PathType.Component*/);
1351
+ assert(this.type === ExprPathType.Event/* || this.type === PathType.Component*/);
1472
1352
  assert(root instanceof HTMLElement);
1473
1353
  /*#ENDIF*/
1474
1354
 
1475
1355
  let eventName = this.attrName.slice(2); // remove "on-" prefix.
1476
1356
  let func;
1477
-
1478
- // Convert array to function.
1479
1357
  let args = [];
1480
- if (Array.isArray(expr)) {
1481
-
1482
- // oninput=${[this.doSomething, 'meow']}
1483
- if (typeof expr[0] === 'function') {
1484
- func = expr[0];
1485
- args = expr.slice(1);
1486
- }
1487
1358
 
1488
- // oninput=${[this, 'value']}
1489
- else {
1490
- func = setValue;
1491
- args = [expr[0], expr.slice(1), node];
1492
- node.value = delve(expr[0], expr.slice(1));
1493
- // root.render(); // TODO: This causes infinite recursion.
1494
- }
1359
+ // Convert array to function.
1360
+ // oninput=${[this.doSomething, 'meow']}
1361
+ if (Array.isArray(expr) && typeof expr[0] === 'function') {
1362
+ func = expr[0];
1363
+ args = expr.slice(1);
1495
1364
  }
1496
- else
1365
+ else if (typeof expr === 'function')
1497
1366
  func = expr;
1367
+ else
1368
+ throw new Error(`Invalid event binding: <${node.tagName.toLowerCase()} ${this.attrName}=\${${JSON.stringify(expr)}}>`);
1498
1369
 
1499
1370
  this.bindEvent(node, root, eventName, eventName, func, args);
1500
1371
  }
1501
1372
 
1502
1373
 
1374
+ /**
1375
+ * Call function when eventName is triggerd on node.
1376
+ * @param node {HTMLElement}
1377
+ * @param root {HTMLElement}
1378
+ * @param key {string}
1379
+ * @param eventName {string}
1380
+ * @param func {function}
1381
+ * @param args {array}
1382
+ * @param capture {boolean} */
1503
1383
  bindEvent(node, root, key, eventName, func, args, capture=false) {
1504
- let nodeEvents = Globals.nodeEvents.get(node);
1384
+ let nodeEvents = Globals$1.nodeEvents.get(node);
1505
1385
  if (!nodeEvents) {
1506
1386
  nodeEvents = {[key]: new Array(3)};
1507
- Globals.nodeEvents.set(node, nodeEvents);
1387
+ Globals$1.nodeEvents.set(node, nodeEvents);
1508
1388
  }
1509
1389
  let nodeEvent = nodeEvents[key];
1510
1390
  if (!nodeEvent)
1511
1391
  nodeEvents[key] = nodeEvent = new Array(3);
1512
1392
 
1393
+ if (typeof func !== 'function')
1394
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${${func}}> because it's not a function.`);
1513
1395
 
1514
1396
  // If function has changed, remove and rebind the event.
1515
1397
  if (nodeEvent[0] !== func) {
1516
1398
 
1399
+ // TODO: We should be removing event listeners when calling getNodeGroup(),
1400
+ // when we get the node from the list of nodeGroupsAttached/nodeGroupsDetached,
1401
+ // instead of only when we rebind an event.
1517
1402
  let [existing, existingBound, _] = nodeEvent;
1518
1403
  if (existing)
1519
1404
  node.removeEventListener(eventName, existingBound, capture);
@@ -1543,68 +1428,144 @@ class ExprPath {
1543
1428
  nodeEvents[key][2] = args;
1544
1429
  }
1545
1430
 
1546
- applyValueAttrib(node, exprs, exprIndex) {
1547
- let expr = exprs[exprIndex];
1431
+ /**
1432
+ * Handle values, including two-way binding.
1433
+ * @param node
1434
+ * @param exprs */
1435
+ // TODO: node is always this.nodeMarker?
1436
+ applyValueAttrib(node, exprs) {
1437
+ let expr = exprs[0];
1548
1438
 
1549
- // Values to toggle an attribute
1550
- if (!this.attrValue && (expr === false || expr === null || expr === undefined))
1551
- node.removeAttribute(this.attrName);
1439
+ // Two-way binding between attributes
1440
+ // Passing a path to the value attribute.
1441
+ // Copies the attribute to the property when the input event fires.
1442
+ // value=${[this, 'value]'}
1443
+ // checked=${[this, 'isAgree']}
1444
+ // This same logic is in NodeGroup.instantiateComponent() for components.
1445
+ if (Util.isPath(expr)) {
1446
+ let [obj, path] = [expr[0], expr.slice(1)];
1552
1447
 
1553
- else if (!this.attrValue && expr === true)
1554
- node.setAttribute(this.attrName, '');
1448
+ if (!obj)
1449
+ throw new Error(`Solarite cannot bind to <${node.tagName.toLowerCase()} ${this.attrName}=\${[${expr.map(item => item ? `'${item}'` : item+'').join(', ')}]}>.`);
1450
+
1451
+ let value = delve(obj, path);
1452
+
1453
+ // Special case to allow setting select-multiple value from an array
1454
+ if (this.attrName === 'value' && node.type === 'select-multiple' && Array.isArray(value)) {
1455
+ // Set the .selected property on the options having a value within value.
1456
+ let strValues = value.map(v => v + '');
1457
+ for (let option of node.options)
1458
+ option.selected = strValues.includes(option.value);
1459
+ }
1460
+ else {
1461
+ // TODO: should we remove isFalsy, since these are always props?
1462
+ let strValue = Util.isFalsy(value) ? '' : value;
1555
1463
 
1556
- // Passing a path to the value attribute.
1557
- // This same logic is in NodeGroup.createNewComponent() for components.
1558
- else if ((this.attrName === 'value' || this.attrName === 'data-value') && Util.isPath(expr)) {
1559
- let [obj, path] = [expr[0], expr.slice(1)];
1560
- node.value = delve(obj, path);
1561
- // TODO: We need to remove any old listeners, like in bindEventAttribute
1464
+ // If we don't have this condition, when we call render(), the browser will scroll to the currently
1465
+ // selected item in a <select> and mess up manually scrolling to a different value.
1466
+ if (strValue !== node[this.attrName])
1467
+ node[this.attrName] = strValue;
1468
+ }
1562
1469
 
1470
+ // TODO: We need to remove any old listeners, like in bindEventAttribute.
1471
+ // Does bindEvent() now handle that?
1563
1472
  let func = () => {
1564
- delve(obj, path, Util.getInputValue(node));
1473
+ let value = (this.attrName === 'value')
1474
+ ? Util.getInputValue(node)
1475
+ : node[this.attrName];
1476
+ delve(obj, path, value);
1565
1477
  };
1566
1478
 
1567
1479
  // We use capture so we update the values before other events added by the user.
1568
- this.bindEvent(node, path[0], 'value', 'input', func, [], true);
1480
+ // TODO: Bind to scroll events also?
1481
+ // What about resize events and width/height?
1482
+ this.bindEvent(node, path[0], this.attrName, 'input', func, [], true);
1569
1483
  }
1570
1484
 
1571
1485
  // Regular attribute
1572
1486
  else {
1573
- let value = [];
1574
-
1575
- // We go backward because NodeGroup.applyExprs() calls this function, and it goes backward through the exprs.
1576
- if (this.attrValue) {
1577
- for (let i=this.attrValue.length-1; i>=0; i--) {
1578
- value.unshift(this.attrValue[i]);
1579
- if (i > 0) {
1580
- let val = exprs[exprIndex];
1581
- if (val !== false && val !== null && val !== undefined)
1582
- value.unshift(val);
1583
- exprIndex--;
1487
+ // TODO: Cache this on ExprPath.isProp when Shell creates the props. Have ExprPath.clone() copy .isProp
1488
+ // Or make it a new PathType.
1489
+ //if (this.attrName === 'disabled')
1490
+ // debugger;
1491
+
1492
+ // hasOwnProperty() checks only the object, not the parents
1493
+ // this.attrName in node checks the node and the parents.
1494
+ // This version checks the html element it extends from, to see if has a setter set:
1495
+ // Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set
1496
+ //let isProp = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(node), this.attrName)?.set;
1497
+ let isProp = this.isHtmlProperty;
1498
+ if (isProp === undefined)
1499
+ isProp = this.isHtmlProperty = Util.isHtmlProp(node, this.attrName);
1500
+
1501
+ // Values to toggle an attribute
1502
+ let multiple = this.attrValue;
1503
+ if (!multiple) {
1504
+ Globals$1.currentExprPath = this; // Used by watch()
1505
+ if (typeof expr === 'function') {
1506
+ if (this.type === 4) { // Don't evaluate functions before passing them to components
1507
+ return
1584
1508
  }
1509
+ this.watchFunction = expr; // The function that gets the expression, used for renderWatched()
1510
+ expr = expr();
1585
1511
  }
1586
- exprIndex ++;
1512
+ else
1513
+ expr = Util.makePrimitive(expr);
1514
+ Globals$1.currentExprPath = null;
1515
+ }
1516
+ if (!multiple && (expr === undefined || expr === false || expr === null)) { // Util.isFalsy() inlined.
1517
+ if (isProp)
1518
+ node[this.attrName] = false;
1519
+ node.removeAttribute(this.attrName);
1520
+ }
1521
+ else if (!multiple && expr === true) {
1522
+ if (isProp)
1523
+ node[this.attrName] = true;
1524
+ node.setAttribute(this.attrName, '');
1587
1525
  }
1588
- else
1589
- value.unshift(expr);
1590
1526
 
1591
- let joinedValue = value.join('');
1527
+ // A non-toggled attribute
1528
+ else {
1592
1529
 
1593
- // Only update attributes if the value has changed.
1594
- // The .value property is special. If it changes we don't update the attribute.
1595
- let oldVal = this.attrName === 'value' ? node.value : node.getAttribute(this.attrName);
1596
- if (oldVal !== joinedValue) {
1597
- node.setAttribute(this.attrName, joinedValue);
1598
- }
1530
+ // If it's a series of expressions among strings, join them together.
1531
+ let joinedValue;
1532
+ if (multiple) {
1533
+ let value = [];
1534
+ for (let i = 0; i < this.attrValue.length; i++) {
1535
+ value.push(this.attrValue[i]);
1536
+ if (i < this.attrValue.length - 1) {
1537
+ Globals$1.currentExprPath = this; // Used by watch()
1538
+ let val = Util.makePrimitive(exprs[i]);
1539
+ Globals$1.currentExprPath = null;
1540
+ if (!Util.isFalsy(val))
1541
+ value.push(val);
1542
+ }
1543
+ }
1544
+ joinedValue = value.join('');
1545
+ }
1599
1546
 
1600
- // This is needed for setting input.value, .checked, option.selected, etc.
1601
- // But in some cases setting the attribute is enough. such as div.setAttribute('title') updates div.title.
1602
- // TODO: How to tell which is which?
1603
- if (this.attrName in node)
1604
- node[this.attrName] = joinedValue;
1547
+ // If the attribute is one expression with no strings:
1548
+ else
1549
+ joinedValue = expr;
1550
+
1551
+ // Only update attributes if the value has changed.
1552
+ // This is needed for setting input.value, .checked, option.selected, etc.
1553
+
1554
+ let oldVal = isProp
1555
+ ? node[this.attrName]
1556
+ : node.getAttribute(this.attrName);
1557
+ if (oldVal !== joinedValue) {
1558
+
1559
+ // <textarea value=${expr}></textarea>
1560
+ // Without this branch we have no way to set the value of a textarea,
1561
+ // since we also prohibit expressions that are a child of textarea.
1562
+ if (isProp)
1563
+ node[this.attrName] = joinedValue;
1564
+ // TODO: Putting an 'else' here would be more performant
1565
+ node.setAttribute(this.attrName, joinedValue);
1566
+ }
1567
+ }
1605
1568
  }
1606
-
1607
- return exprIndex;
1608
1569
  }
1609
1570
 
1610
1571
 
@@ -1620,7 +1581,8 @@ class ExprPath {
1620
1581
  let nodeMarker, nodeBefore;
1621
1582
  let root = newRoot;
1622
1583
  let path = pathOffset ? this.nodeMarkerPath.slice(0, -pathOffset) : this.nodeMarkerPath;
1623
- for (let i=path.length-1; i>0; i--) // Resolve the path.
1584
+ let length = path.length-1;
1585
+ for (let i=length; i>0; i--) // Resolve the path.
1624
1586
  root = root.childNodes[path[i]];
1625
1587
  let childNodes = root.childNodes;
1626
1588
 
@@ -1665,7 +1627,7 @@ class ExprPath {
1665
1627
 
1666
1628
  /**
1667
1629
  * Attempt to remove all of this ExprPath's nodes from the DOM, if it can be done using a special fast method.
1668
- * @returns {boolean} Returns false if Nodes werne't removed, and they should instead be removed manually. */
1630
+ * @returns {boolean} Returns false if Nodes weren't removed, and they should instead be removed manually. */
1669
1631
  fastClear() {
1670
1632
  let parent = this.nodeBefore.parentNode;
1671
1633
  if (this.nodeBefore === parent.firstChild && this.nodeMarker === parent.lastChild) {
@@ -1701,6 +1663,10 @@ class ExprPath {
1701
1663
  // result2.push(...ng.getNodes())
1702
1664
  // return result2;
1703
1665
 
1666
+ if (this.type === ExprPathType.AttribValue || this.type === ExprPathType.AttribMultiple || this.type === ExprPathType.ComponentAttribValue) {
1667
+ return [this.nodeMarker];
1668
+ }
1669
+
1704
1670
 
1705
1671
  let result;
1706
1672
 
@@ -1727,7 +1693,8 @@ class ExprPath {
1727
1693
  return result;
1728
1694
  }
1729
1695
 
1730
- getParentNode() { // Same as this.parentNode
1696
+ /** @return {HTMLElement|ParentNode} */
1697
+ getParentNode() {
1731
1698
  return this.nodeMarker.parentNode
1732
1699
  }
1733
1700
 
@@ -1744,28 +1711,39 @@ class ExprPath {
1744
1711
  * or createa new NodeGroup from the template.
1745
1712
  * @return {NodeGroup} */
1746
1713
  getNodeGroup(template, exact=true) {
1747
- //if (exact && this.nodeGroupsFree.isEmpty())
1748
- // return null;
1749
1714
 
1750
1715
  let result;
1716
+ let collection = this.nodeGroupsAttachedAvailable;
1751
1717
 
1752
1718
  // TODO: Would it be faster to maintain a separate list of detached nodegroups?
1753
1719
  if (exact) { // [below] parentElement will be null if the parent is a DocumentFragment
1754
- result = this.nodeGroupsFree.deletePreferred(template.getExactKey(), ng=>ng.startNode.parentElement);
1720
+ result = collection.deleteAny(template.getExactKey());
1721
+ if (!result) { // try searching detached
1722
+ collection = this.nodeGroupsDetachedAvailable;
1723
+ result = collection.deleteAny(template.getExactKey());
1724
+ }
1725
+
1755
1726
  if (result) // also delete the matching close key.
1756
- this.nodeGroupsFree.delete(template.getCloseKey(), result);
1757
- else
1727
+ collection.deleteSpecific(template.getCloseKey(), result);
1728
+ else {
1758
1729
  return null;
1730
+ }
1759
1731
  }
1760
1732
 
1761
1733
  // Find a close match.
1762
1734
  // This is a match that has matching html, but different expressions applied.
1763
1735
  // We can then apply the expressions to make it an exact match.
1764
- else {
1765
- result = this.nodeGroupsFree.deletePreferred(template.getCloseKey(), ng=>ng.startNode.parentElement);
1736
+ // 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.
1737
+ else if (template.exprs.length) {
1738
+ result = collection.deleteAny(template.getCloseKey());
1739
+ if (!result) { // try searching detached
1740
+ collection = this.nodeGroupsDetachedAvailable;
1741
+ result = collection.deleteAny(template.getCloseKey());
1742
+ }
1743
+
1766
1744
  if (result) {
1767
1745
  /*#IFDEV*/assert(result.exactKey);/*#ENDIF*/
1768
- this.nodeGroupsFree.delete(result.exactKey, result);
1746
+ collection.deleteSpecific(result.exactKey, result);
1769
1747
 
1770
1748
  // Update this close match with the new expression values.
1771
1749
  result.applyExprs(template.exprs);
@@ -1777,64 +1755,70 @@ class ExprPath {
1777
1755
  result = new NodeGroup(template, this);
1778
1756
 
1779
1757
  // old:
1780
- this.nodeGroupsInUse.push(result);
1781
-
1782
- // new:
1783
- // let ngiu = this.nodeGroupsInUse;
1784
- // ngiu.add(result.exactKey, result);
1785
- // ngiu.add(result.closeKey, result);
1758
+ this.nodeGroupsRendered.push(result);
1786
1759
 
1787
1760
  /*#IFDEV*/assert(result.parentPath);/*#ENDIF*/
1788
1761
  return result;
1789
1762
  }
1790
1763
 
1764
+ isComponent() {
1765
+ // Events won't have type===Component.
1766
+ // TODO: Have a special flag for components instead of it being on the type?
1767
+ return this.type === ExprPathType.ComponentAttribValue || (this.attrName && this.nodeMarker.tagName && this.nodeMarker.tagName.includes('-'));
1768
+ }
1791
1769
 
1792
1770
  /**
1771
+ * TODO: Rename this to nodeGroupsInUse, nodeGroupsAvialableAttached and nodeGroupsAvailableDetached?
1772
+ * Nodes that have been used during the current render().
1793
1773
  * Used with getNodeGroup() and freeNodeGroups().
1794
1774
  * TODO: Use an array of WeakRef so the gc can collect them?
1795
1775
  * TODO: Put items back in nodeGroupsInUse after applyExpr() is called, not before.
1796
1776
  * @type {NodeGroup[]} */
1797
- nodeGroupsInUse = [];
1798
-
1799
- /** @type {MultiValueMap<key:string, value:NodeGroup>} */
1800
- //nodeGroupsInUse = new MultiValueMap();
1777
+ nodeGroupsRendered = [];
1801
1778
 
1802
1779
  /**
1780
+ * Nodes that were added to the web component during the last render(), but are available to be used again.
1803
1781
  * Used with getNodeGroup() and freeNodeGroups().
1804
1782
  * Each NodeGroup is here twice, once under an exact key, and once under the close key.
1805
1783
  * @type {MultiValueMap<key:string, value:NodeGroup>} */
1806
- nodeGroupsFree = new MultiValueMap();
1784
+ nodeGroupsAttachedAvailable = new MultiValueMap();
1807
1785
 
1808
- nodeGroupsDetached = new MultiValueMap();
1786
+ /**
1787
+ * Nodes that were not added to the web component during the last render(), and available to be used again.
1788
+ * @type {MultiValueMap} */
1789
+ nodeGroupsDetachedAvailable = new MultiValueMap();
1809
1790
 
1810
1791
 
1811
1792
  /**
1812
- * Move everything from this.nodeGroupsInUse to this.nodeGroupsFree.
1793
+ * Move everything from this.nodeGroupsRendered to this.nodeGroupsAttached and nodeGroupsDetached.
1794
+ * Called at the beginning of applyNodes() so it can have NodeGroups to use.
1813
1795
  * TODO: this could run as needed in getNodeGroup? */
1814
1796
  freeNodeGroups() {
1815
- // old:
1816
-
1817
- //this.nodeGroupsDetached = this.nodeGroupsFree;
1818
- //this.nodeGroupsFree = new MultiValueMap();
1797
+ // Add nodes that weren't used during render() to nodeGroupsDetached
1798
+ let previouslyAttached = this.nodeGroupsAttachedAvailable.data;
1799
+ let detached = this.nodeGroupsDetachedAvailable.data;
1800
+ for (let key in previouslyAttached) {
1801
+ let set = detached[key];
1802
+ if (!set)
1803
+ detached[key] = previouslyAttached[key];
1804
+ else
1805
+ for (let ng of previouslyAttached[key])
1806
+ set.add(ng);
1807
+ }
1819
1808
 
1820
- let ngf = this.nodeGroupsFree;
1821
- for (let ng of this.nodeGroupsInUse) {
1822
- ngf.add(ng.exactKey, ng);
1823
- ngf.add(ng.closeKey, ng);
1809
+ // Add nodes that were used during render() to nodeGroupsRendered.
1810
+ this.nodeGroupsAttachedAvailable = new MultiValueMap();
1811
+ let nga = this.nodeGroupsAttachedAvailable;
1812
+ for (let ng of this.nodeGroupsRendered) {
1813
+ nga.add(ng.exactKey, ng);
1814
+ nga.add(ng.closeKey, ng);
1824
1815
  }
1825
- this.nodeGroupsInUse = [];
1826
1816
 
1827
- // new:
1828
- // for (let key in this.nodeGroupsFree.data)
1829
- // for (let item of this.nodeGroupsFree.data[key])
1830
- // this.nodeGroupsInUse.add(key, item);
1831
- //
1832
- // this.nodeGroupsFree = this.nodeGroupsInUse;
1833
- // this.nodeGroupsInUse = new MultiValueMap();
1817
+ this.nodeGroupsRendered = [];
1834
1818
  }
1835
1819
 
1836
1820
  //#IFDEV
1837
-
1821
+
1838
1822
  get debug() {
1839
1823
  return [
1840
1824
  `parentNode: ${this.nodeBefore.parentNode?.tagName?.toLowerCase()}`,
@@ -1847,7 +1831,7 @@ class ExprPath {
1847
1831
  }), 1).flat()
1848
1832
  ]
1849
1833
  }
1850
-
1834
+
1851
1835
  get debugNodes() {
1852
1836
  // Clear nodesCache so that getNodes() manually gets the nodes.
1853
1837
  let nc = this.nodesCache;
@@ -1856,13 +1840,13 @@ class ExprPath {
1856
1840
  this.nodesCache = nc;
1857
1841
  return result;
1858
1842
  }
1859
-
1843
+
1860
1844
  verify() {
1861
1845
  if (!window.verify)
1862
1846
  return;
1863
1847
 
1864
- assert(this.type!==PathType.Content || this.nodeBefore);
1865
- assert(this.type!==PathType.Content || this.nodeBefore.parentNode);
1848
+ assert(this.type!==ExprPathType.Content || this.nodeBefore);
1849
+ assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode);
1866
1850
 
1867
1851
  // Need either nodeMarker or parentNode
1868
1852
  assert(this.nodeMarker);
@@ -1871,10 +1855,10 @@ class ExprPath {
1871
1855
  assert(!this.nodeMarker || this.nodeMarker.parentNode);
1872
1856
 
1873
1857
  // nodeBefore and nodeMarker must have same parent.
1874
- assert(this.type!==PathType.Content || this.nodeBefore.parentNode === this.nodeMarker.parentNode);
1858
+ assert(this.type!==ExprPathType.Content || this.nodeBefore.parentNode === this.nodeMarker.parentNode);
1875
1859
 
1876
1860
  assert(this.nodeBefore !== this.nodeMarker);
1877
- assert(this.type!==PathType.Content|| !this.nodeBefore.parentNode || this.nodeBefore.compareDocumentPosition(this.nodeMarker) === Node.DOCUMENT_POSITION_FOLLOWING);
1861
+ assert(this.type!==ExprPathType.Content|| !this.nodeBefore.parentNode || this.nodeBefore.compareDocumentPosition(this.nodeMarker) === Node.DOCUMENT_POSITION_FOLLOWING);
1878
1862
 
1879
1863
  // Detect cyclic parent and grandparent references.
1880
1864
  assert(this.parentNg?.parentPath !== this);
@@ -1887,43 +1871,27 @@ class ExprPath {
1887
1871
  // Make sure the nodesCache matches the nodes.
1888
1872
  this.checkNodesCache();
1889
1873
  }
1890
-
1874
+
1891
1875
  checkNodesCache() {
1892
1876
  return;
1893
1877
  }
1894
1878
  //#ENDIF
1895
1879
  }
1896
1880
 
1897
-
1898
-
1899
- /**
1900
- *
1901
- * @param root
1902
- * @param path {string[]}
1903
- * @param node {HTMLElement}
1904
- */
1905
- function setValue(root, path, node) {
1906
- let val = node.value;
1907
- if (node.type === 'number')
1908
- val = parseFloat(val);
1909
-
1910
- delve(root, path, val);
1911
- }
1912
-
1913
1881
  /** @enum {int} */
1914
- const PathType = {
1882
+ const ExprPathType = {
1915
1883
  /** Child of a node */
1916
1884
  Content: 1,
1917
-
1885
+
1918
1886
  /** One or more whole attributes */
1919
- Multiple: 2,
1920
-
1887
+ AttribMultiple: 2,
1888
+
1921
1889
  /** Value of an attribute. */
1922
- Value: 3,
1923
-
1890
+ AttribValue: 3,
1891
+
1924
1892
  /** Value of an attribute being passed to a component. */
1925
- Component: 4,
1926
-
1893
+ ComponentAttribValue: 4,
1894
+
1927
1895
  /** Expressions inside Html comments. */
1928
1896
  Comment: 5,
1929
1897
 
@@ -1949,118 +1917,179 @@ function getNodePath(node) {
1949
1917
  * Note that the path is backward, with the outermost element at the end.
1950
1918
  * @param root {HTMLElement|Document|DocumentFragment|ParentNode}
1951
1919
  * @param path {int[]}
1952
- * @returns {Node|HTMLElement} */
1920
+ * @returns {Node|HTMLElement|HTMLStyleElement} */
1953
1921
  function resolveNodePath(root, path) {
1954
1922
  for (let i=path.length-1; i>=0; i--)
1955
1923
  root = root.childNodes[path[i]];
1956
1924
  return root;
1957
1925
  }
1958
1926
 
1927
+ class HtmlParser {
1928
+ constructor() {
1929
+ this.defaultState = {
1930
+ context: HtmlParser.Text, // possible values: 'TEXT', 'TAG', 'ATTRIBUTE'
1931
+ quote: null, // possible values: null, '"', "'"
1932
+ buffer: '',
1933
+ lastChar: null
1934
+ };
1935
+ this.state = {...this.defaultState};
1936
+ }
1937
+
1938
+ reset() {
1939
+ this.state = {...this.defaultState};
1940
+ return this.state.context;
1941
+ }
1942
+
1943
+ /**
1944
+ * Parse the next chunk of html, starting with the same context we left off with from the previous chunk.
1945
+ * @param html {string}
1946
+ * @param onContextChange {?function(html:string, index:int, oldContext:string, newContext:string)}
1947
+ * Called every time the context changes, and again at the last context.
1948
+ * @return {('Attribute','Text','Tag')} The context at the end of html. */
1949
+ parse(html, onContextChange=null) {
1950
+ if (html === null)
1951
+ return this.reset();
1952
+
1953
+ for (let i = 0; i < html.length; i++) {
1954
+ const char = html[i];
1955
+ switch (this.state.context) {
1956
+ case HtmlParser.Text:
1957
+ if (char === '<' && html[i + 1].match(/[/a-z!]/i)) { // Start of a tag or comment.
1958
+ onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
1959
+ this.state.context = HtmlParser.Tag;
1960
+ this.state.buffer = '';
1961
+ }
1962
+ break;
1963
+ case HtmlParser.Tag:
1964
+ if (char === '>') {
1965
+ onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
1966
+ this.state.context = HtmlParser.Text;
1967
+ this.state.quote = null;
1968
+ this.state.buffer = '';
1969
+ }
1970
+ else if (char === ' ' && !this.state.buffer) {
1971
+ // No attribute name is present. Skipping the space.
1972
+ continue;
1973
+ }
1974
+ else if (char === ' ' || char === '/' || char === '?') {
1975
+ this.state.buffer = ''; // Reset the buffer when a delimiter or potential self-closing sign is found.
1976
+ }
1977
+ else if (char === '"' || char === "'" || char === '=') {
1978
+ onContextChange?.(html, i, this.state.context, HtmlParser.Attribute);
1979
+ this.state.context = HtmlParser.Attribute;
1980
+ this.state.quote = char === '=' ? null : char;
1981
+ this.state.buffer = '';
1982
+ }
1983
+ else
1984
+ this.state.buffer += char;
1985
+ break;
1986
+ case HtmlParser.Attribute:
1987
+ // Start an attribute quote.
1988
+ if (!this.state.quote && !this.state.buffer.length && (char === '"' || char === "'")) {
1989
+ this.state.quote = char;
1990
+ }
1991
+ else if (char === this.state.quote || (!this.state.quote && this.state.buffer.length)) {
1992
+ onContextChange?.(html, i, this.state.context, HtmlParser.Tag);
1993
+ this.state.context = HtmlParser.Tag;
1994
+ this.state.quote = null;
1995
+ this.state.buffer = '';
1996
+ }
1997
+ else if (!this.state.quote && char === '>') {
1998
+ onContextChange?.(html, i+1, this.state.context, HtmlParser.Text);
1999
+ this.state.context = HtmlParser.Text;
2000
+ this.state.quote = null;
2001
+ this.state.buffer = '';
2002
+ }
2003
+ else if (char !== ' ')
2004
+ this.state.buffer += char;
2005
+
2006
+ break;
2007
+ }
2008
+ }
2009
+ onContextChange?.(html, html.length, this.state.context, null);
2010
+ return this.state.context;
2011
+ }
2012
+ }
2013
+
2014
+ HtmlParser.Attribute = 'Attribute';
2015
+ HtmlParser.Text = 'Text';
2016
+ HtmlParser.Tag = 'Tag';
2017
+
1959
2018
  /**
1960
2019
  * A Shell is created from a tagged template expression instantiated as Nodes,
1961
2020
  * but without any expressions filled in.
1962
2021
  * Only one Shell is created for all the items in a loop.
1963
2022
  *
1964
2023
  * When a NodeGroup is created from a Template's html strings,
1965
- * the NodeGroup then clones the Shell's fragmentn to be its nodes. */
2024
+ * the NodeGroup then clones the Shell's fragment to be its nodes. */
1966
2025
  class Shell {
1967
2026
 
1968
2027
  /**
1969
- * @type {DocumentFragment} DOM parent of the shell nodes. */
2028
+ * @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
1970
2029
  fragment;
1971
2030
 
1972
2031
  /** @type {ExprPath[]} Paths to where expressions should go. */
1973
2032
  paths = [];
1974
2033
 
1975
- // Embeds and ids
1976
- events = [];
2034
+ // Elements with events. Not yet used.
2035
+ // events = [];
1977
2036
 
1978
2037
  /** @type {int[][]} Array of paths */
1979
2038
  ids = [];
2039
+
2040
+ /** @type {int[][]} Array of paths */
1980
2041
  scripts = [];
2042
+
2043
+ /** @type {int[][]} Array of paths */
1981
2044
  styles = [];
1982
2045
 
2046
+ /** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
1983
2047
  staticComponents = [];
1984
2048
 
2049
+ /** @type {{path:int[], attribs:Record<string, string>}[]} */
2050
+ //componentAttribs = [];
2051
+
1985
2052
 
1986
2053
 
1987
2054
  /**
1988
2055
  * Create the nodes but without filling in the expressions.
1989
2056
  * This is useful because the expression-less nodes created by a template can be cached.
1990
- * @param html {string[]} */
2057
+ * @param html {string[]} Html strings, split on places where an expression exists. */
1991
2058
  constructor(html=null) {
1992
2059
  if (!html)
1993
2060
  return;
1994
2061
 
1995
2062
  //#IFDEV
1996
- this.html = html.join('');
2063
+ this._html = html.join('');
1997
2064
  //#ENDIF
1998
2065
 
1999
- // 1. Add placeholders
2000
- // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
2001
- let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
2002
-
2003
- let buffer = [];
2004
- let commentPlaceholder = `<!--!✨!-->`;
2005
- let componentNames = {};
2006
-
2007
- htmlContext(null); // Reset the context.
2008
- for (let i=0; i<html.length; i++) {
2009
- let lastHtml = html[i];
2010
- let context = htmlContext(lastHtml);
2011
-
2012
- // Swap out Embedded Solarite Components with ${} attributes.
2013
- // Later, NodeGroup.render() will search for these and replace them with the real components.
2014
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2015
- if (context === htmlContext.Attribute) {
2016
-
2017
- let lastIndex, lastMatch;
2018
- lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
2019
- lastIndex = index+1; // +1 for after opening <
2020
- lastMatch = match.slice(1);
2021
- });
2022
-
2023
- if (lastMatch) {
2024
- let newTagName = lastMatch + '-solarite-placeholder';
2025
- lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
2026
- componentNames[lastMatch] = newTagName;
2027
- }
2028
- }
2029
-
2030
- buffer.push(lastHtml);
2031
- //console.log(lastHtml, context)
2032
- if (i < html.length-1)
2033
- if (context === htmlContext.Text)
2034
- buffer.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
2035
- else
2036
- buffer.push(String.fromCharCode(placeholder+i));
2066
+ if (html.length === 1 && !html[0].match(/[<&]/)) {
2067
+ this.fragment = document.createTextNode(html[0]);
2068
+ return;
2037
2069
  }
2038
2070
 
2039
- // 2. Create elements from html with placeholders.
2040
- let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
2041
- let joinedHtml = buffer.join('');
2042
2071
 
2043
- // Replace '-solarite-placeholder' close tags.
2044
- // TODO: is there a better way? What if the close tag is inside a comment?
2045
- for (let name in componentNames)
2046
- joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
2047
-
2048
- if (joinedHtml)
2049
- template.innerHTML = joinedHtml;
2050
- else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
2051
- template.content.append(document.createTextNode(''));
2072
+ // 1. Add placeholders
2073
+ let joinedHtml = Shell.addPlaceholders(html);
2074
+
2075
+ let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
2076
+ if (joinedHtml)
2077
+ template.innerHTML = joinedHtml;
2078
+ else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
2079
+ template.content.append(document.createTextNode(''));
2052
2080
  this.fragment = template.content;
2053
2081
 
2054
- // 3. Find placeholders
2082
+ // 2. Find placeholders
2055
2083
  let node;
2056
2084
  let toRemove = [];
2057
- const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
2085
+ let placeholdersUsed = 0;
2086
+ const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
2058
2087
  while (node = walker.nextNode()) {
2059
2088
 
2060
2089
  // Remove previous after each iteration, so paths will still be calculated correctly.
2061
2090
  toRemove.map(el => el.remove());
2062
2091
  toRemove = [];
2063
-
2092
+
2064
2093
  // Replace attributes
2065
2094
  if (node.nodeType === 1) {
2066
2095
  for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
@@ -2068,7 +2097,8 @@ class Shell {
2068
2097
  // Whole attribute
2069
2098
  let matches = attr.name.match(/^[\ue000-\uf8ff]$/);
2070
2099
  if (matches) {
2071
- this.paths.push(new ExprPath(null, node, PathType.Multiple));
2100
+ this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
2101
+ placeholdersUsed ++;
2072
2102
  node.removeAttribute(matches[0]);
2073
2103
  }
2074
2104
 
@@ -2077,16 +2107,17 @@ class Shell {
2077
2107
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
2078
2108
  if (parts.length > 1) {
2079
2109
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
2080
- let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
2110
+ let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
2081
2111
 
2082
2112
  this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
2113
+ placeholdersUsed += parts.length - 1;
2083
2114
  node.setAttribute(attr.name, parts.join(''));
2084
2115
  }
2085
2116
  }
2086
2117
  }
2087
2118
  }
2088
2119
  // Replace comment placeholders
2089
- else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
2120
+ else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
2090
2121
 
2091
2122
  // Get or create nodeBefore.
2092
2123
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
@@ -2111,12 +2142,14 @@ class Shell {
2111
2142
  }
2112
2143
  /*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
2113
2144
 
2114
-
2115
-
2116
- let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
2117
-
2145
+ let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
2118
2146
  this.paths.push(path);
2147
+ placeholdersUsed ++;
2119
2148
  }
2149
+
2150
+ else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
2151
+ throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
2152
+
2120
2153
 
2121
2154
 
2122
2155
  // Sometimes users will comment out a block of html code that has expressions.
@@ -2127,8 +2160,9 @@ class Shell {
2127
2160
  let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
2128
2161
  for (let i=0; i<parts.length-1; i++) {
2129
2162
  let path = new ExprPath(node.previousSibling, node);
2130
- path.type = PathType.Comment;
2163
+ path.type = ExprPathType.Comment;
2131
2164
  this.paths.push(path);
2165
+ placeholdersUsed ++;
2132
2166
  }
2133
2167
  }
2134
2168
 
@@ -2146,8 +2180,9 @@ class Shell {
2146
2180
  }
2147
2181
 
2148
2182
  for (let i=0, node; node=placeholders[i]; i++) {
2149
- let path = new ExprPath(node.previousSibling, node, PathType.Content);
2183
+ let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
2150
2184
  this.paths.push(path);
2185
+ placeholdersUsed ++;
2151
2186
 
2152
2187
  /*#IFDEV*/path.verify();/*#ENDIF*/
2153
2188
  }
@@ -2159,17 +2194,17 @@ class Shell {
2159
2194
  }
2160
2195
  toRemove.map(el => el.remove());
2161
2196
 
2197
+ // Less than or equal because there can be one path to multiple expressions
2198
+ // if those expressions are in the same attribute value.
2199
+ if (placeholdersUsed !== html.length-1)
2200
+ throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
2201
+
2162
2202
  // Handle solarite-placeholder's.
2163
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2164
- //if (componentNames.size)
2165
- // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
2166
2203
 
2167
- // Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
2204
+ // 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
2168
2205
  // that happens in NodeGroup.applyComponentExprs()
2169
- for (let el of this.fragment.querySelectorAll('[is]')) {
2206
+ for (let el of this.fragment.querySelectorAll('[is]'))
2170
2207
  el.setAttribute('_is', el.getAttribute('is'));
2171
- // this.components.push(el);
2172
- }
2173
2208
 
2174
2209
  for (let path of this.paths) {
2175
2210
  if (path.nodeBefore)
@@ -2177,16 +2212,64 @@ class Shell {
2177
2212
  path.nodeMarkerPath = getNodePath(path.nodeMarker);
2178
2213
 
2179
2214
  // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
2180
- if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
2215
+ if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
2181
2216
  (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
2182
- path.type = PathType.Component;
2217
+ path.type = ExprPathType.ComponentAttribValue;
2183
2218
  }
2184
2219
  }
2185
2220
 
2186
2221
  this.findEmbeds();
2187
2222
 
2188
2223
  /*#IFDEV*/this.verify();/*#ENDIF*/
2189
- } // end constructor
2224
+ }
2225
+
2226
+ /**
2227
+ * 1. Add a Unicode placeholder char for where expressions go within attributes.
2228
+ * 2. Add a comment placeholder for where expressions are children of other nodes.
2229
+ * 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
2230
+ * @param htmlChunks {string[]}
2231
+ * @returns {string} */
2232
+ static addPlaceholders(htmlChunks) {
2233
+ let tokens = [];
2234
+
2235
+ function addToken(token, context) {
2236
+
2237
+ if (context === HtmlParser.Tag) {
2238
+ // Find Solarite Components tags and append -solarite-placeholder to their tag names
2239
+ // and give them a solarite-placeholder attribute so we can easily find them later.
2240
+ // This way we can gather their constructor arguments and their children before we call their constructor.
2241
+ // Later, NodeGroup.instantiateComponent() will replace them with the real components.
2242
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2243
+ token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder solarite-placeholder');
2244
+ }
2245
+ tokens.push(token);
2246
+ }
2247
+
2248
+ let htmlParser = new HtmlParser(); // Reset the context.
2249
+ for (let i = 0; i < htmlChunks.length; i++) {
2250
+ let lastHtml = htmlChunks[i];
2251
+
2252
+ // Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
2253
+ let lastIndex = 0;
2254
+ let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
2255
+ if (lastIndex !== index) {
2256
+ let token = html.slice(lastIndex, index);
2257
+ addToken(token, oldContext);
2258
+ }
2259
+ lastIndex = index;
2260
+ });
2261
+
2262
+ // Insert placeholders
2263
+ if (i < htmlChunks.length - 1) {
2264
+ if (context === HtmlParser.Text)
2265
+ tokens.push(commentPlaceholder); // Comment Placeholder. because we can't put text in between <tr> tags for example.
2266
+ else
2267
+ tokens.push(String.fromCharCode(attribPlaceholder + i));
2268
+ }
2269
+ }
2270
+
2271
+ return tokens.join('');
2272
+ }
2190
2273
 
2191
2274
  /**
2192
2275
  * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
@@ -2198,36 +2281,30 @@ class Shell {
2198
2281
  * this.staticComponents */
2199
2282
  findEmbeds() {
2200
2283
  this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el));
2284
+
2285
+ // TODO: only find styles that have ExprPaths in them?
2201
2286
  this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el));
2202
2287
 
2203
2288
  let idEls = this.fragment.querySelectorAll('[id],[data-id]');
2204
-
2205
2289
 
2206
2290
  // Check for valid id names.
2207
2291
  for (let el of idEls) {
2208
2292
  let id = el.getAttribute('data-id') || el.getAttribute('id');
2209
- if (div.hasOwnProperty(id))
2293
+ if (Globals$1.div.hasOwnProperty(id))
2210
2294
  throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
2211
2295
  }
2212
2296
 
2213
-
2214
2297
  this.ids = Array.prototype.map.call(idEls, el => getNodePath(el));
2215
2298
 
2216
- // Events (not yet used)
2217
2299
  for (let el of this.fragment.querySelectorAll('*')) {
2218
- for (let attrib of el.attributes)
2219
- if (isEvent(attrib.name))
2220
- this.events.push([attrib.name, getNodePath(el)]);
2221
-
2222
2300
  if (el.tagName.includes('-') || el.hasAttribute('_is'))
2223
2301
 
2224
- // Dynamic components have attributes with expression values.
2302
+ // Dynamic components are components that have attributes with expression values.
2225
2303
  // They are created from applyExprs()
2226
2304
  // But static components are created in a separate path inside the NodeGroup constructor.
2227
2305
  if (!this.paths.find(path => path.nodeMarker === el))
2228
2306
  this.staticComponents.push(getNodePath(el));
2229
2307
  }
2230
-
2231
2308
  }
2232
2309
 
2233
2310
  /**
@@ -2235,10 +2312,10 @@ class Shell {
2235
2312
  * @param htmlStrings {string[]} Typically comes from a Template.
2236
2313
  * @returns {Shell} */
2237
2314
  static get(htmlStrings) {
2238
- let result = Globals.shells.get(htmlStrings);
2315
+ let result = Globals$1.shells.get(htmlStrings);
2239
2316
  if (!result) {
2240
2317
  result = new Shell(htmlStrings);
2241
- Globals.shells.set(htmlStrings, result); // cache
2318
+ Globals$1.shells.set(htmlStrings, result); // cache
2242
2319
  }
2243
2320
 
2244
2321
  /*#IFDEV*/result.verify();/*#ENDIF*/
@@ -2254,7 +2331,14 @@ class Shell {
2254
2331
  }
2255
2332
  }
2256
2333
  //#ENDIF
2257
- }
2334
+ }
2335
+
2336
+
2337
+ const commentPlaceholder = `<!--!✨!-->`;
2338
+
2339
+
2340
+ // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
2341
+ const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
2258
2342
 
2259
2343
  /** @typedef {boolean|string|number|function|Object|Array|Date|Node|Template} Expr */
2260
2344
 
@@ -2280,7 +2364,8 @@ class NodeGroup {
2280
2364
  startNode;
2281
2365
 
2282
2366
  /** @type {Node|HTMLElement} A node that never changes that this NodeGroup should always insert its nodes before.
2283
- * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.*/
2367
+ * An empty text node will be created to insertBefore if there's no other NodeMarker and this isn't at the last position.
2368
+ * TODO: But sometimes startNode and endNode point to the same node. Document htis inconsistency. */
2284
2369
  endNode;
2285
2370
 
2286
2371
  /** @type {ExprPath[]} */
@@ -2298,11 +2383,11 @@ class NodeGroup {
2298
2383
  nodesCache;
2299
2384
 
2300
2385
  /**
2386
+ * A map between <style> Elements and their text content.
2387
+ * This lets NodeGroup.updateStyles() see when the style text has changed.
2301
2388
  * @type {?Map<HTMLStyleElement, string>} */
2302
2389
  styles;
2303
2390
 
2304
- currentComponentProps = {};
2305
-
2306
2391
 
2307
2392
  /**
2308
2393
  * Create an "instantiated" NodeGroup from a Template and add it to an element.
@@ -2310,14 +2395,26 @@ class NodeGroup {
2310
2395
  * @param parentPath {?ExprPath} */
2311
2396
  constructor(template, parentPath=null) {
2312
2397
  if (!(this instanceof RootNodeGroup)) {
2398
+
2313
2399
  let [fragment, shell] = this.init(template, parentPath);
2314
2400
 
2315
- this.updatePaths(fragment, shell.paths);
2401
+ if (fragment && template.exprs.length) {
2402
+ this.updatePaths(fragment, shell.paths);
2316
2403
 
2317
- this.activateEmbeds(fragment, shell);
2404
+ // Static web components can sometimes have children created via expressions.
2405
+ // But calling applyExprs() will mess up the shell's path to them.
2406
+ // So we find them first, then call activateStaticComponents() after their children have been created.
2407
+ let staticComponents = this.findStaticComponents(fragment, shell);
2318
2408
 
2319
- // Apply exprs
2320
- this.applyExprs(template.exprs);
2409
+ this.activateEmbeds(fragment, shell);
2410
+
2411
+ // Apply exprs
2412
+ this.applyExprs(template.exprs);
2413
+
2414
+ this.instantiateStaticComponents(staticComponents);
2415
+ }
2416
+ else if (shell)
2417
+ this.activateEmbeds(fragment, shell);
2321
2418
  }
2322
2419
  }
2323
2420
 
@@ -2345,68 +2442,119 @@ class NodeGroup {
2345
2442
  template.nodeGroup = this;
2346
2443
 
2347
2444
  // Get a cached version of the parsed and instantiated html, and ExprPaths.
2348
- let shell = Shell.get(template.html);
2349
- let fragment = shell.fragment.cloneNode(true);
2350
2445
 
2351
- let childNodes = fragment.childNodes;
2352
- this.startNode = childNodes[0];
2353
- this.endNode = childNodes[childNodes.length - 1];
2446
+ // If it's just a text node, skip a bunch of unnecessary steps.
2447
+ if (!(this instanceof RootNodeGroup) && !template.exprs.length && !template.html[0].includes('<')) {
2448
+ //let doc = this.rootNg.startNode?.ownerDocument || document;
2449
+ let textNode = document.createTextNode(template.html[0]);
2354
2450
 
2355
- return [fragment, shell];
2451
+ this.startNode = this.endNode = textNode;
2452
+ return [];
2453
+ }
2454
+ else {
2455
+ let shell = Shell.get(template.html);
2456
+ let fragment = shell.fragment.cloneNode(true);
2457
+
2458
+ if (fragment instanceof DocumentFragment) {
2459
+ let childNodes = fragment.childNodes;
2460
+ this.startNode = childNodes[0];
2461
+ this.endNode = childNodes[childNodes.length - 1];
2462
+ }
2463
+ else {
2464
+ this.startNode = this.endNode = fragment;
2465
+ }
2466
+ return [fragment, shell];
2467
+ }
2356
2468
  }
2357
2469
 
2358
2470
  /**
2359
2471
  * Use the paths to insert the given expressions.
2360
2472
  * Dispatches expression handling to other functions depending on the path type.
2361
2473
  * @param exprs {(*|*[]|function|Template)[]}
2362
- * @param paths {?ExprPath[]} Optional. */
2474
+ * @param paths {?ExprPath[]} Optional. Only used for testing. Normally uses this.paths. */
2363
2475
  applyExprs(exprs, paths=null) {
2364
2476
  paths = paths || this.paths;
2365
2477
 
2366
- /*#IFDEV*/this.verify();/*#ENDIF*/
2478
+ /*#IFDEV*/
2479
+ this.verify();/*#ENDIF*/
2480
+
2481
+ // Things to consider:
2482
+ // 1. One path may use multipe expressions. E.g. <div class="${1} ${2}">
2483
+ // 2. One component may need to use multiple attribute paths to be instantiated.
2484
+ // 3. We apply them in reverse order so that a <select> box has its children created from an expression
2485
+ // before its instantiated and its value attribute is set via an expression.
2486
+
2487
+ let exprIndex = exprs.length - 1; // Update exprs at paths.
2488
+ let lastComponentPathIndex;
2489
+ 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.
2490
+ for (let i = paths.length - 1, path; path = paths[i]; i--) {
2491
+ let prevPath = paths[i - 1];
2492
+ let nextPath = paths[i + 1];
2493
+
2494
+ // Get the expressions associated with this path.
2495
+ if (path.attrValue?.length > 2) {
2496
+ let startIndex = (exprIndex - (path.attrValue.length - 1)) + 1;
2497
+ pathExprs[i] = exprs.slice(startIndex, exprIndex + 1); // probably doesn't allocate if the JS vm implements copy on write.
2498
+ exprIndex -= pathExprs[i].length;
2499
+ } else {
2500
+ pathExprs[i] = [exprs[exprIndex]];
2501
+ exprIndex--;
2502
+ }
2367
2503
 
2368
- // Update exprs at paths.
2369
- let exprIndex = exprs.length-1, expr, lastNode;
2504
+ // TODO: Need to end and restart this block when going from one component to the next?
2505
+ // Think of having two adjacent components.
2506
+ // But the dynamicAttribsAdjacet test already passes.
2370
2507
 
2371
- // We apply them in reverse order so that a <select> box has its options created from an expression
2372
- // before its value attribute is set via an expression.
2373
- for (let path of paths.toReversed()) {
2374
- expr = exprs[exprIndex];
2508
+ // If expr is an attribute in a component:
2509
+ // 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
2510
+ // 2. Otherwise send them to its render function.
2511
+ // Components with no expressions as attributes are instead activated in activateEmbeds().
2512
+ if (path.nodeMarker !== this.rootNg.root && path.isComponent()) {
2375
2513
 
2376
- // Nodes
2514
+ if (!nextPath || !nextPath.isComponent() || nextPath.nodeMarker !== path.nodeMarker)
2515
+ lastComponentPathIndex = i;
2516
+ let isFirstComponentPath = !prevPath || !prevPath.isComponent() || prevPath.nodeMarker !== path.nodeMarker;
2377
2517
 
2378
- // This is necessary both here and below.
2379
- if (lastNode && lastNode !== this.rootNg.root && lastNode !== path.nodeMarker && Object.keys(this.currentComponentProps).length) {
2380
- this.applyComponentExprs(lastNode, this.currentComponentProps);
2381
- this.currentComponentProps = {};
2382
- }
2518
+ if (isFirstComponentPath) {
2383
2519
 
2384
- exprIndex = path.apply(expr, exprs, exprIndex, this.currentComponentProps);
2520
+ let componentProps = {};
2521
+ for (let j=i; j<=lastComponentPathIndex; j++) {
2522
+ let attrName = paths[j].attrName; // Util.dashesToCamel(paths[j].attrName);
2523
+ componentProps[attrName] = pathExprs[j].length > 1 ? pathExprs[j].join('') : pathExprs[j][0];
2524
+ }
2385
2525
 
2386
- lastNode = path.nodeMarker;
2526
+ this.applyComponentExprs(path.nodeMarker, componentProps);
2387
2527
 
2528
+ // Set attributes on component.
2529
+ for (let j=i; j<=lastComponentPathIndex; j++)
2530
+ paths[j].apply(pathExprs[j]);
2531
+ }
2532
+ }
2388
2533
 
2389
- exprIndex--;
2390
- } // end for(path of this.paths)
2534
+ // Else apply it normally
2535
+ else
2536
+ path.apply(pathExprs[i]);
2391
2537
 
2392
2538
 
2393
- // Check again after we iterate through all paths to apply to a component.
2394
- if (lastNode && lastNode !== this.rootNg.root && Object.keys(this.currentComponentProps).length) {
2395
- this.applyComponentExprs(lastNode, this.currentComponentProps);
2396
- this.currentComponentProps = {};
2397
- }
2539
+ } // end for(path of this.paths)
2398
2540
 
2541
+
2542
+ // TODO: Only do this if we have ExprPaths within styles?
2399
2543
  this.updateStyles();
2400
2544
 
2545
+
2546
+
2401
2547
  // Invalidate the nodes cache because we just changed it.
2402
2548
  this.nodesCache = null;
2403
2549
 
2404
2550
  // If there's leftover expressions, there's probably an issue with the Shell that created this NodeGroup,
2405
2551
  // and the number of paths not matching.
2406
- /*#IFDEV*/assert(exprIndex === -1);/*#ENDIF*/
2552
+ /*#IFDEV*/
2553
+ assert(exprIndex === -1);/*#ENDIF*/
2407
2554
 
2408
2555
 
2409
- /*#IFDEV*/this.verify();/*#ENDIF*/
2556
+ /*#IFDEV*/
2557
+ this.verify();/*#ENDIF*/
2410
2558
  }
2411
2559
 
2412
2560
  /**
@@ -2420,24 +2568,30 @@ class NodeGroup {
2420
2568
  // then we could re-use the hash and logic from NodeManager?
2421
2569
  let newHash = getObjectHash(props);
2422
2570
 
2423
- let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
2571
+ let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
2424
2572
  let isPreIsElement = el.hasAttribute('_is');
2425
2573
 
2426
2574
 
2427
2575
  // Instantiate a placeholder.
2428
2576
  if (isPreHtmlElement || isPreIsElement)
2429
- el = this.createNewComponent(el, isPreHtmlElement, props);
2577
+ el = this.instantiateComponent(el, isPreHtmlElement, props);
2430
2578
 
2431
2579
  // Call render() with the same params that would've been passed to the constructor.
2580
+ // We do this even if the arguments haven't changed, so we can let the child component
2581
+ // compare the arguments and then decide for itself whether it wants to re-render.
2432
2582
  else if (el.render) {
2433
- let oldHash = Globals.componentHash.get(el);
2434
- if (oldHash !== newHash)
2435
- el.render(props); // Pass new values of props to render so it can decide how it wants to respond.
2583
+ //let oldHash = Globals.componentArgsHash.get(el);
2584
+ //if (oldHash !== newHash) { // Only if not changed.
2585
+ let args = {};
2586
+ for (let name in props || {})
2587
+ args[Util.dashesToCamel(name)] = props[name];
2588
+ el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
2589
+ //}
2436
2590
  }
2437
2591
 
2438
- Globals.componentHash.set(el, newHash);
2592
+ Globals$1.componentArgsHash.set(el, newHash);
2439
2593
  }
2440
-
2594
+
2441
2595
  /**
2442
2596
  * We swap the placeholder element for the real element so we can pass its dynamic attributes
2443
2597
  * to its constructor.
@@ -2445,78 +2599,54 @@ class NodeGroup {
2445
2599
  * The logic of this function is complex and could use cleaning up.
2446
2600
  *
2447
2601
  * @param el
2448
- * @param isPreHtmlElement
2602
+ * @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
2449
2603
  * @param props {Object} Attributes with dynamic values.
2450
2604
  * @return {HTMLElement} */
2451
- createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
2605
+ instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
2452
2606
  if (isPreHtmlElement === undefined)
2453
2607
  isPreHtmlElement = !el.hasAttribute('_is');
2454
-
2608
+
2455
2609
  let tagName = (isPreHtmlElement
2456
- ? el.tagName.endsWith('-SOLARITE-PLACEHOLDER')
2457
- ? el.tagName.slice(0, -21)
2458
- : el.tagName
2610
+ ? el.tagName.slice(0, -21) // Remove -SOLARITE-PLACEHOLDER
2459
2611
  : el.getAttribute('is')).toLowerCase();
2460
2612
 
2461
- let dynamicProps = {...(props || {})};
2462
-
2613
+
2614
+ // Throw if custom element isn't defined.
2615
+ let Constructor = customElements.get(tagName);
2616
+ if (!Constructor)
2617
+ throw new Error(`The custom tag name ${tagName} is not registered.`)
2618
+
2619
+ let args = {};
2620
+ for (let name in props || {})
2621
+ args[Util.dashesToCamel(name)] = props[name];
2622
+
2463
2623
  // Pass other attribs to constructor, since otherwise they're not yet set on the element,
2464
2624
  // and the constructor would otherwise have no way to see them.
2465
2625
  if (el.attributes.length) {
2466
- if (!props)
2467
- props = {};
2468
- for (let attrib of el.attributes)
2469
- if (!props.hasOwnProperty(attrib.name))
2470
- props[attrib.name] = attrib.value;
2626
+ for (let attrib of el.attributes) {
2627
+ let attribName = Util.dashesToCamel(attrib.name);
2628
+ if (!args.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
2629
+ args[attribName] = attrib.value;
2630
+ }
2471
2631
  }
2472
-
2473
- // Create CustomElement and
2474
- let Constructor = customElements.get(tagName);
2475
- if (!Constructor)
2476
- throw new Error(`The custom tag name ${tagName} is not registered.`)
2477
2632
 
2478
- // We pass the childNodes to the constructor so it can know about them,
2479
- // instead of only afterward when they're appended to the slot below.
2480
- // This is useful for a custom selectbox, for example.
2481
- // Globals.pendingChildren stores the childen so the super construtor call to Solarite's constructor
2482
- // can add them as children before the rest of the constructor code executes.
2483
- let ch = [... el.childNodes];
2484
- Globals.pendingChildren.push(ch); // pop() is called in Solarite constructor.
2485
- let newEl = new Constructor(props, ch);
2633
+ // Create the web component.
2634
+ // Get the children that aren't Solarite's comment placeholders.
2635
+ let ch = [...el.childNodes].filter(node => node.nodeType !== Node.COMMENT_NODE || !node.nodeValue.startsWith('ExprPath'));
2636
+ let newEl = new Constructor(args, ch);
2486
2637
 
2487
2638
  if (!isPreHtmlElement)
2488
2639
  newEl.setAttribute('is', el.getAttribute('is').toLowerCase());
2640
+
2641
+ // Replace the placeholder tag with the instantiated web component.
2489
2642
  el.replaceWith(newEl);
2490
2643
 
2491
- // Set children / slot children
2492
- // TODO: Match named slots.
2493
- // TODO: This only appends to slot if render() is called in the constructor.
2494
- //let slot = newEl.querySelector('slot') || newEl;
2495
- //slot.append(...el.childNodes);
2496
-
2497
- // Copy over event attributes.
2498
- for (let propName in props) {
2499
- let val = props[propName];
2500
- if (propName.startsWith('on') && typeof val === 'function')
2501
- newEl.addEventListener(propName.slice(2), e => val(e, newEl));
2502
-
2503
- // Bind array based event attributes on value.
2504
- // This same logic is in ExprPath.applyValueAttrib() for non-components.
2505
- if ((propName === 'value' || propName === 'data-value') && Util.isPath(val)) {
2506
- let [obj, path] = [val[0], val.slice(1)];
2507
- newEl.value = delve(obj, path);
2508
- newEl.addEventListener('input', e => {
2509
- delve(obj, path, Util.getInputValue(newEl));
2510
- }, true); // We use capture so we update the values before other events added by the user.
2511
- }
2512
- }
2513
-
2514
2644
  // If an id pointed at the placeholder, update it to point to the new element.
2515
2645
  let id = el.getAttribute('data-id') || el.getAttribute('id');
2516
2646
  if (id)
2517
2647
  delve(this.getRootNode(), id.split(/\./g), newEl);
2518
-
2519
-
2648
+
2649
+
2520
2650
  // Update paths to use replaced element.
2521
2651
  for (let path of this.paths) {
2522
2652
  if (path.nodeMarker === el)
@@ -2528,31 +2658,31 @@ class NodeGroup {
2528
2658
  this.startNode = newEl;
2529
2659
  if (this.endNode === el)
2530
2660
  this.endNode = newEl;
2531
-
2532
-
2661
+
2662
+
2533
2663
  // applyComponentExprs() is called because we're rendering.
2534
2664
  // So we want to render the sub-component also.
2535
2665
  if (newEl.renderFirstTime)
2536
2666
  newEl.renderFirstTime();
2537
-
2667
+
2538
2668
  // Copy attributes over.
2539
2669
  for (let attrib of el.attributes)
2540
- if (attrib.name !== '_is')
2670
+ if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
2541
2671
  newEl.setAttribute(attrib.name, attrib.value);
2542
2672
 
2543
2673
  // Set dynamic attributes if they are primitive types.
2544
- for (let name in dynamicProps) {
2545
- let val = dynamicProps[name];
2674
+ for (let name in props) {
2675
+ let val = props[name];
2546
2676
  if (typeof val === 'boolean') {
2547
2677
  if (val !== false && val !== undefined && val !== null)
2548
2678
  newEl.setAttribute(name, '');
2549
2679
  }
2550
2680
 
2551
- // If type isn't an object or array, set the attribute.
2681
+ // If type is a non-boolean primitive, set the attribute value.
2552
2682
  else if (['number', 'bigint', 'string'].includes(typeof val))
2553
2683
  newEl.setAttribute(name, val);
2554
2684
  }
2555
-
2685
+
2556
2686
  return newEl;
2557
2687
  }
2558
2688
 
@@ -2598,8 +2728,7 @@ class NodeGroup {
2598
2728
 
2599
2729
  /**
2600
2730
  * Requires the nodeCache to be present. */
2601
- saveOrphans() {
2602
- /*#IFDEV*/assert(!this.startNode.parentNode);/*#ENDIF*/
2731
+ removeAndSaveOrphans() {
2603
2732
  /*#IFDEV*/assert(this.nodesCache);/*#ENDIF*/
2604
2733
  let fragment = document.createDocumentFragment();
2605
2734
  for (let node of this.getNodes())
@@ -2609,8 +2738,9 @@ class NodeGroup {
2609
2738
 
2610
2739
  updatePaths(fragment, paths, offset) {
2611
2740
  // Update paths to point to the fragment.
2612
- this.paths.length = paths.length;
2613
- for (let i=0; i<paths.length; i++) {
2741
+ let pathLength = paths.length;
2742
+ this.paths.length = pathLength;
2743
+ for (let i=0; i<pathLength; i++) {
2614
2744
  let path = paths[i].clone(fragment, offset);
2615
2745
  path.parentNg = this;
2616
2746
  this.paths[i] = path;
@@ -2632,23 +2762,23 @@ class NodeGroup {
2632
2762
  * An interleaved array of sets of nodes and top-level ExprPaths
2633
2763
  * @type {(Node|HTMLElement|ExprPath)[]} */
2634
2764
  get nodes() { throw new Error('')};
2635
-
2765
+
2636
2766
  get debug() {
2637
2767
  return [
2638
2768
  `parentNode: ${this.parentNode?.tagName?.toLowerCase()}`,
2639
2769
  'nodes:',
2640
2770
  ...setIndent(this.getNodes().map(item => {
2641
2771
  if (item instanceof Node) {
2642
-
2772
+
2643
2773
  let tree = nodeToArrayTree(item, nextNode => {
2644
-
2645
- let path = this.paths.find(path=>path.type === PathType.Content && path.getNodes().includes(nextNode));
2774
+
2775
+ let path = this.paths.find(path=>path.type === ExprPathType.Content && path.getNodes().includes(nextNode));
2646
2776
  if (path)
2647
2777
  return [`Path.nodes:`]
2648
-
2778
+
2649
2779
  return [];
2650
2780
  });
2651
-
2781
+
2652
2782
  // TODO: How to indend nodes belonging to a path vs those that just occur after the path?
2653
2783
  return flattenAndIndent(tree)
2654
2784
  }
@@ -2659,10 +2789,10 @@ class NodeGroup {
2659
2789
  }
2660
2790
 
2661
2791
  get debugNodes() { return this.getNodes() }
2662
-
2663
-
2792
+
2793
+
2664
2794
  get debugNodesHtml() { return this.getNodes().map(n => n.outerHTML || n.textContent) }
2665
-
2795
+
2666
2796
  verify() {
2667
2797
  if (!window.verify)
2668
2798
  return;
@@ -2677,7 +2807,7 @@ class NodeGroup {
2677
2807
 
2678
2808
  // if (this.parentPath)
2679
2809
  // assert(this.parentPath.nodeGroups.includes(this));
2680
-
2810
+
2681
2811
  for (let path of this.paths) {
2682
2812
  assert(path.parentNg === this);
2683
2813
 
@@ -2693,61 +2823,70 @@ class NodeGroup {
2693
2823
  }
2694
2824
  //#ENDIF
2695
2825
 
2826
+ findStaticComponents(root, shell, pathOffset=0) {
2827
+ let result = [];
2696
2828
 
2697
- /**
2698
- * @param root {HTMLElement}
2699
- * @param shell {Shell}
2700
- * @param pathOffset {int} */
2701
- activateEmbeds(root, shell, pathOffset=0) {
2702
-
2703
- // static components. These are WebComponents not created by an expression.
2704
- // Must happen before ids.
2829
+ // static components. These are WebComponents that do not have any constructor arguments that are expressions.
2830
+ // Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
2831
+ // Maybe someday these two paths will be merged?
2832
+ // Must happen before ids because instantiateComponent will replace the element.
2705
2833
  for (let path of shell.staticComponents) {
2706
2834
  if (pathOffset)
2707
2835
  path = path.slice(0, -pathOffset);
2708
2836
  let el = resolveNodePath(root, path);
2709
2837
 
2710
2838
  // Shell doesn't know if a web component is the pseudoRoot so we have to detect it here.
2839
+ // Recreating it is necessary so we can pass the constructor args to it.
2711
2840
  if (root !== el/* && !isReplaceEl(root, el)*/) // TODO: is isReplaceEl necessary?
2712
- this.createNewComponent(el);
2841
+ result.push(el);
2713
2842
  }
2843
+ return result;
2844
+ }
2845
+
2846
+ instantiateStaticComponents(staticComponents) {
2847
+ for (let el of staticComponents)
2848
+ this.instantiateComponent(el);
2849
+ }
2850
+
2851
+ /**
2852
+ * @param root {HTMLElement|DocumentFragment}
2853
+ * @param shell {Shell}
2854
+ * @param pathOffset {int} */
2855
+ activateEmbeds(root, shell, pathOffset=0) {
2714
2856
 
2715
2857
  let rootEl = this.rootNg.root;
2716
2858
  if (rootEl) {
2859
+ let options = this.rootNg.options;
2717
2860
 
2718
2861
  // ids
2719
- if (this.options?.ids !== false)
2862
+ if (options?.ids !== false) {
2720
2863
  for (let path of shell.ids) {
2721
2864
  if (pathOffset)
2722
2865
  path = path.slice(0, -pathOffset);
2723
2866
  let el = resolveNodePath(root, path);
2724
- let id = el.getAttribute('data-id') || el.getAttribute('id');
2725
- if (id) { // If something hasn't removed the id.
2726
-
2727
- // Don't allow overwriting existing class properties if they already have a non-Node value.
2728
- if (rootEl[id] && !(rootEl[id] instanceof Node))
2729
- throw new Error(`${rootEl.constructor.name}.${id} already has a value. ` +
2730
- `Can't set it as a reference to <${el.tagName.toLowerCase()} id="${id}">`);
2731
-
2732
- delve(rootEl, id.split(/\./g), el);
2733
- }
2867
+ Util.bindId(rootEl, el);
2868
+ }
2734
2869
  }
2735
2870
 
2736
2871
  // styles
2737
- if (this.options?.styles !== false) {
2872
+ if (options?.styles !== false) {
2738
2873
  if (shell.styles.length)
2739
2874
  this.styles = new Map();
2740
2875
  for (let path of shell.styles) {
2741
2876
  if (pathOffset)
2742
2877
  path = path.slice(0, -pathOffset);
2878
+
2879
+ /** @type {HTMLStyleElement} */
2743
2880
  let style = resolveNodePath(root, path);
2744
- Util.bindStyles(style, rootEl);
2745
- this.styles.set(style, style.textContent);
2881
+ if (rootEl.nodeType === 1) {
2882
+ Util.bindStyles(style, rootEl);
2883
+ this.styles.set(style, style.textContent);
2884
+ }
2746
2885
  }
2747
2886
 
2748
2887
  }
2749
2888
  // scripts
2750
- if (this.options?.scripts !== false) {
2889
+ if (options?.scripts !== false) {
2751
2890
  for (let path of shell.scripts) {
2752
2891
  if (pathOffset)
2753
2892
  path = path.slice(0, -pathOffset);
@@ -2768,20 +2907,8 @@ class RootNodeGroup extends NodeGroup {
2768
2907
  root;
2769
2908
 
2770
2909
  /**
2771
- * Store the expressions that use this watched variable,
2772
- * along with the functions used to get their values.
2773
- * @type {Object<field:string, Set<ExprPath>>} */
2774
- watchedExprPaths = {};
2775
-
2776
- /**
2777
- * Map from arrays where .map is called and their callback functions.
2778
- * TODO: One array might be called with two different map functions in different places!
2779
- * @type {Map<Array, function>} */
2780
- mapCallbacks = new Map();
2781
-
2782
- /**
2783
- *
2784
- * @type {Map<ExprPath, boolean|Array>} */
2910
+ * When we call renerWatched() we re-render these expressions, then clear this to a new Map()
2911
+ * @type {Map<ExprPath, ValueOp|WholeArrayOp|ArraySpliceOp[]>} */
2785
2912
  exprsToRender = new Map();
2786
2913
 
2787
2914
  /**
@@ -2798,82 +2925,102 @@ class RootNodeGroup extends NodeGroup {
2798
2925
  this.rootNg = this;
2799
2926
  let [fragment, shell] = this.init(template);
2800
2927
 
2801
- // If adding NodeGroup to an element.
2802
- let offset = 0;
2803
- let root = fragment; // TODO: Rename so it's not confused with this.root.
2804
- if (el) {
2805
- Globals.nodeGroups.set(el, this);
2806
-
2807
- // Save slot children
2808
- let slotFragment;
2809
- if (el.childNodes.length) {
2810
- slotFragment = document.createDocumentFragment();
2811
- slotFragment.append(...el.childNodes);
2928
+ if (fragment instanceof Text) {
2929
+
2930
+ if (el) {
2931
+ this.startNode = el;
2932
+ this.endNode = el;
2933
+ if (fragment.nodeValue.length)
2934
+ el.append(fragment);
2935
+ this.root = el;
2812
2936
  }
2937
+ Globals$1.nodeGroups.set(this.root, this);
2938
+ }
2939
+ else {
2813
2940
 
2814
- this.root = el;
2941
+ // If adding NodeGroup to an element.
2942
+ let offset = 0;
2943
+ let root = fragment; // TODO: Rename so it's not confused with this.root.
2944
+ if (el) {
2945
+ Globals$1.nodeGroups.set(el, this);
2946
+
2947
+ // Save slot children
2948
+ let slotChildren;
2949
+ if (el.childNodes.length) {
2950
+ slotChildren = document.createDocumentFragment();
2951
+ slotChildren.append(...el.childNodes);
2952
+ }
2815
2953
 
2816
- // If el should replace the root node of the fragment.
2817
- if (isReplaceEl(fragment, el)) {
2818
- el.append(...fragment.children[0].childNodes);
2954
+ this.root = el;
2819
2955
 
2820
- // Copy attributes
2821
- for (let attrib of fragment.children[0].attributes)
2822
- if (!el.hasAttribute(attrib.name))
2823
- el.setAttribute(attrib.name, attrib.value);
2956
+ // If el should replace the root node of the fragment.
2957
+ if (isReplaceEl(fragment, el)) {
2958
+ el.append(...fragment.children[0].childNodes);
2824
2959
 
2825
- // Go one level deeper into all of shell's paths.
2826
- offset = 1;
2827
- }
2828
- else {
2829
- let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2830
- if (!isEmpty)
2831
- el.append(...fragment.childNodes);
2832
- }
2960
+ // Copy attributes
2961
+ for (let attrib of fragment.children[0].attributes)
2962
+ if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
2963
+ el.setAttribute(attrib.name, attrib.value);
2964
+
2965
+ // Go one level deeper into all of shell's paths.
2966
+ offset = 1;
2967
+ } else {
2968
+ let isEmpty = fragment.childNodes.length === 1 && fragment.childNodes[0].nodeType === 3 && fragment.childNodes[0].textContent === '';
2969
+ if (!isEmpty)
2970
+ el.append(...fragment.childNodes);
2971
+ }
2972
+
2973
+ // Setup children
2974
+ if (slotChildren) {
2975
+
2976
+ // Named slots
2977
+ for (let slot of el.querySelectorAll('slot[name]')) {
2978
+ let name = slot.getAttribute('name');
2979
+ if (name) {
2980
+ let slotChildren2 = slotChildren.querySelectorAll(`[slot='${name}']`);
2981
+ slot.append(...slotChildren2);
2982
+ }
2983
+ }
2984
+
2985
+ // Unnamed slots
2986
+ let unamedSlot = el.querySelector('slot:not([name])');
2987
+ if (unamedSlot)
2988
+ unamedSlot.append(slotChildren);
2833
2989
 
2834
- // Setup slots
2835
- if (slotFragment) {
2836
- for (let slot of el.querySelectorAll('slot[name]')) {
2837
- let name = slot.getAttribute('name');
2838
- if (name) {
2839
- let slotChildren = slotFragment.querySelectorAll(`[slot='${name}']`);
2840
- slot.append(...slotChildren);
2841
- }
2990
+ // No slots
2991
+ else
2992
+ el.append(slotChildren);
2842
2993
  }
2843
- let unamedSlot = el.querySelector('slot:not([name])');
2844
- if (unamedSlot)
2845
- unamedSlot.append(slotFragment);
2846
- else
2847
- el.append(slotFragment);
2848
- }
2849
2994
 
2850
- root = el;
2995
+ root = el;
2851
2996
 
2852
- this.startNode = el;
2853
- this.endNode = el;
2854
- }
2855
- else {
2856
- let singleEl = getSingleEl(fragment);
2857
- this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
2997
+ this.startNode = el;
2998
+ this.endNode = el;
2999
+ } else {
3000
+ let singleEl = getSingleEl(fragment);
3001
+ this.root = singleEl || fragment; // We return the whole fragment when calling r() with a collection of nodes.
2858
3002
 
2859
- Globals.nodeGroups.set(this.root, this);
2860
- if (singleEl) {
2861
- root = singleEl;
2862
- offset = 1;
3003
+ Globals$1.nodeGroups.set(this.root, this);
3004
+ if (singleEl) {
3005
+ root = singleEl;
3006
+ offset = 1;
3007
+ }
2863
3008
  }
2864
- }
2865
3009
 
2866
- this.updatePaths(root, shell.paths, offset);
3010
+ this.updatePaths(root, shell.paths, offset);
2867
3011
 
2868
- this.activateEmbeds(root, shell, offset);
3012
+ // Static web components can sometimes have children created via expressions.
3013
+ // But calling applyExprs() will mess up the shell's path to them.
3014
+ // So we find them first, then call activateStaticComponents() after their children have been created.
3015
+ let staticComponents = this.findStaticComponents(root, shell, offset);
2869
3016
 
2870
- // Apply exprs
2871
- this.applyExprs(template.exprs);
2872
- }
3017
+ this.activateEmbeds(root, shell, offset);
3018
+
3019
+ // Apply exprs
3020
+ this.applyExprs(template.exprs);
2873
3021
 
2874
- clearRenderWatched() {
2875
- this.watchedExprPaths = {};
2876
- this.mapCallbacks = new Map();
3022
+ this.instantiateStaticComponents(staticComponents);
3023
+ }
2877
3024
  }
2878
3025
  }
2879
3026
 
@@ -2895,8 +3042,8 @@ function getSingleEl(fragment) {
2895
3042
  * @param el {HTMLElement}
2896
3043
  * @returns {boolean} */
2897
3044
  function isReplaceEl(fragment, el) {
2898
- return el.tagName.includes('-')
2899
- && fragment.children.length===1
3045
+ return fragment.children.length===1
3046
+ && el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
2900
3047
  && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
2901
3048
  }
2902
3049
 
@@ -2915,19 +3062,9 @@ class Template {
2915
3062
  /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
2916
3063
  hashedFields;
2917
3064
 
2918
- /**
2919
- * @deprecated
2920
- * @type {ExprPath} Used with forEach() from watch.js
2921
- * Set in ExprPath.apply() */
2922
- parentPath;
2923
-
2924
3065
  /** @type {NodeGroup} */
2925
3066
  nodeGroup;
2926
3067
 
2927
- /**
2928
- * @type {string[][]} */
2929
- paths = [];
2930
-
2931
3068
  /**
2932
3069
  *
2933
3070
  * @param htmlStrings {string[]}
@@ -2978,17 +3115,21 @@ class Template {
2978
3115
  if (standalone) {
2979
3116
  ng = new RootNodeGroup(this, null, options);
2980
3117
  el = ng.getRootNode();
2981
- //Globals.nodeGroups.set(el, ng);
3118
+ Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
2982
3119
  firstTime = true;
2983
3120
  }
2984
3121
  else {
2985
- ng = Globals.nodeGroups.get(el);
3122
+ ng = Globals$1.nodeGroups.get(el);
2986
3123
  if (!ng) {
2987
3124
  ng = new RootNodeGroup(this, el, options);
2988
- //Globals.nodeGroups.set(el, ng);
3125
+ Globals$1.nodeGroups.set(el, ng); // Why was this commented out?
2989
3126
  firstTime = true;
2990
3127
  }
2991
- }
3128
+
3129
+ // This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
3130
+ // These don't always have the same length, for example if one attribute has multiple expressions.
3131
+ if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
3132
+ 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.`); }
2992
3133
 
2993
3134
  // Creating the root nodegroup also renders it.
2994
3135
  // If we didn't just create it, we need to render it.
@@ -2996,23 +3137,32 @@ class Template {
2996
3137
  if (this.html?.length === 1 && !this.html[0])
2997
3138
  el.innerHTML = ''; // Fast path for empty component.
2998
3139
  else {
2999
- ng.clearRenderWatched();
3000
3140
  ng.applyExprs(this.exprs);
3001
3141
  }
3002
3142
  }
3003
3143
 
3144
+ ng.exprsToRender = new Map();
3004
3145
  return el;
3005
3146
  }
3006
3147
 
3007
3148
  getExactKey() {
3008
- if (!this.exactKey)
3009
- this.exactKey = getObjectHash(this); // calls this.toJSON().
3149
+ if (!this.exactKey) {
3150
+ if (this.exprs.length)
3151
+ this.exactKey = getObjectHash(this);// calls this.toJSON().
3152
+ else // Don't hash plain html.
3153
+ this.exactKey = this.html[0];
3154
+ }
3010
3155
  return this.exactKey;
3011
3156
  }
3012
3157
 
3013
3158
  getCloseKey() {
3014
- if (!this.closeKey)
3015
- this.closeKey = '@'+this.toJSON()[0];
3159
+ //console.log(this.exprs.length)
3160
+ if (!this.closeKey) {
3161
+ if (this.exprs.length)
3162
+ this.closeKey = /*'@' + */this.toJSON()[0];
3163
+ else
3164
+ this.closeKey = this.html[0];
3165
+ }
3016
3166
  // Use the joined html when debugging? But it breaks some tests.
3017
3167
  //return '@'+this.html.join('|')
3018
3168
 
@@ -3035,8 +3185,8 @@ class Template {
3035
3185
 
3036
3186
  /**
3037
3187
  * Convert strings to HTMLNodes.
3038
- * Using r as a tag will always create a Template.
3039
- * Using r() as a function() will always create a DOM element.
3188
+ * Using h`...` as a tag will always create a Template.
3189
+ * Using h() as a function() will always create a DOM element.
3040
3190
  *
3041
3191
  * Features beyond what standard js tagged template strings do:
3042
3192
  * 1. r`` sub-expressions
@@ -3046,24 +3196,27 @@ class Template {
3046
3196
  * 5. TODO: list more
3047
3197
  *
3048
3198
  * Currently supported:
3049
- * 1. r(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
3050
- * 2. r(el, template, ?options) // Render the Template created by #1 to element.
3199
+ * 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
3200
+ * 2. h(el, template, ?options) // Render the Template created by #1 to element.
3051
3201
  *
3052
- * 3. r`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
3202
+ * 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
3053
3203
  *
3054
- * 4. r('Hello'); // Create single text node.
3055
- * 5. r('<b>Hello</b>'); // Create single HTMLElement
3056
- * 6. r('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3057
- * 7. r()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
3204
+ * 4. h('Hello'); // Create single text node.
3205
+ * 5. h('<b>Hello</b>'); // Create single HTMLElement
3206
+ * 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
3207
+ * 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
3058
3208
  * // includes properly handling nested components and r`` sub-expressions.
3059
- * 8. r(template) // Render Template created by #1.
3060
- *
3061
- * 9. r({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3209
+ * 8. h(template) // Render Template created by #1.
3062
3210
  *
3211
+ * 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
3212
+ * 10. h(string, object, ...) // JSX TODO
3063
3213
  * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
3064
3214
  * @param exprs {*[]|string|Template|Object}
3065
3215
  * @return {Node|HTMLElement|Template} */
3066
- function r(htmlStrings=undefined, ...exprs) {
3216
+ function h(htmlStrings=undefined, ...exprs) {
3217
+
3218
+ if (htmlStrings === undefined && !exprs.length && arguments.length)
3219
+ throw new Error('h() cannot be called with undefined.');
3067
3220
 
3068
3221
  // TODO: Make this a more flat if/else and call other functions for the logic.
3069
3222
  if (htmlStrings instanceof Node) {
@@ -3078,7 +3231,7 @@ function r(htmlStrings=undefined, ...exprs) {
3078
3231
 
3079
3232
  // Return a tagged template function that applies the tagged themplate to parent.
3080
3233
  let taggedTemplate = (htmlStrings, ...exprs) => {
3081
- Globals.rendered.add(parent);
3234
+ Globals$1.rendered.add(parent);
3082
3235
  let template = new Template(htmlStrings, exprs);
3083
3236
  return template.render(parent, options);
3084
3237
  };
@@ -3086,7 +3239,7 @@ function r(htmlStrings=undefined, ...exprs) {
3086
3239
  }
3087
3240
 
3088
3241
  // 2. Render template created by #4 to element.
3089
- else if (exprs[0] instanceof Template) {
3242
+ else { // instanceof Template
3090
3243
  let options = exprs[1];
3091
3244
  template.render(parent, options);
3092
3245
 
@@ -3097,16 +3250,6 @@ function r(htmlStrings=undefined, ...exprs) {
3097
3250
  parent.append(this.rootNg.getParentNode());
3098
3251
  }
3099
3252
  }
3100
-
3101
-
3102
-
3103
- // null for expr[0], remove whole element.
3104
- // This path never happens?
3105
- else {
3106
- throw new Error('unsupported');
3107
- //let ngm = NodeGroupManager.get(parent);
3108
- //ngm.render(null, exprs[1])
3109
- }
3110
3253
  }
3111
3254
 
3112
3255
  // 3. Path if used as a template tag.
@@ -3115,6 +3258,22 @@ function r(htmlStrings=undefined, ...exprs) {
3115
3258
  }
3116
3259
 
3117
3260
  else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
3261
+ // 10. JSX
3262
+ if (typeof exprs[0] === 'object') {
3263
+ exprs[0] || {};
3264
+ exprs.slice(1);
3265
+
3266
+ let templateHtmlStrings = [];
3267
+ let templateExprs = [];
3268
+
3269
+ // TODO How to know which children are static html and which are expression placeholders?
3270
+ // Perhaps we have to treat every text child as a string?
3271
+
3272
+ assert(templateHtmlStrings.length === templateExprs.length+1);
3273
+ return new Template(templateHtmlStrings, templateExprs);
3274
+ }
3275
+
3276
+
3118
3277
  // If it starts with a string, trim both ends.
3119
3278
  // TODO: Also trim if it ends with whitespace?
3120
3279
  if (htmlStrings.match(/^\s^</))
@@ -3138,7 +3297,7 @@ function r(htmlStrings=undefined, ...exprs) {
3138
3297
  else if (htmlStrings === undefined) {
3139
3298
  return (htmlStrings, ...exprs) => {
3140
3299
  //Globals.rendered.add(parent)
3141
- let template = r(htmlStrings, ...exprs);
3300
+ let template = h(htmlStrings, ...exprs);
3142
3301
  return template.render();
3143
3302
  }
3144
3303
  }
@@ -3150,46 +3309,193 @@ function r(htmlStrings=undefined, ...exprs) {
3150
3309
 
3151
3310
 
3152
3311
  // 9. Create dynamic element with render() function.
3312
+ // TODO: This path doesn't handle embeds like data-id="..."
3153
3313
  else if (typeof htmlStrings === 'object') {
3154
3314
  let obj = htmlStrings;
3155
3315
 
3316
+ if (obj.constructor.name !== 'Object')
3317
+ throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
3318
+
3319
+
3156
3320
  // Special rebound render path, called by normal path.
3157
- if (Globals.objToEl.has(obj)) {
3321
+ // Intercepts the main r`...` function call inside render().
3322
+ if (Globals$1.objToEl.has(obj)) {
3158
3323
  return function(...args) {
3159
- let template = r(...args);
3324
+ let template = h(...args);
3160
3325
  let el = template.render();
3161
- Globals.objToEl.set(obj, el);
3326
+ Globals$1.objToEl.set(obj, el);
3162
3327
  }.bind(obj);
3163
3328
  }
3164
3329
 
3165
3330
  // Normal path
3166
3331
  else {
3167
- Globals.objToEl.set(obj, null);
3168
- obj.render(); // Calls the Special rebound render path above, when the render function calls r(this)
3169
- let el = Globals.objToEl.get(obj);
3170
- Globals.objToEl.delete(obj);
3332
+ Globals$1.objToEl.set(obj, null);
3333
+ obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
3334
+ let el = Globals$1.objToEl.get(obj);
3335
+ Globals$1.objToEl.delete(obj);
3171
3336
 
3172
3337
  for (let name in obj)
3173
3338
  if (typeof obj[name] === 'function')
3174
- el[name] = obj[name].bind(el);
3339
+ el[name] = obj[name].bind(el); // Make the "this" of functions be el.
3340
+ // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
3341
+ // <my-element arg=${{myFunc() { return this }}}
3175
3342
  else
3176
3343
  el[name] = obj[name];
3177
3344
 
3345
+ // Bind id's
3346
+ // This doesn't work for id's referenced by attributes.
3347
+ // for (let idEl of el.querySelectorAll('[id],[data-id]')) {
3348
+ // Util.bindId(el, idEl);
3349
+ // Util.bindId(obj, idEl);
3350
+ // }
3351
+ // TODO: Bind styles
3352
+
3178
3353
  return el;
3179
3354
  }
3180
3355
  }
3181
3356
 
3182
3357
  else
3183
3358
  throw new Error('Unsupported arguments.')
3184
- }
3359
+ }
3360
+
3361
+ // Trick to prevent minifier from renaming this function.
3362
+ let renderF = 'render';
3185
3363
 
3186
- //import {watchGet, watchSet} from "./watch.js";
3364
+ /**
3365
+ * There are three ways to create an instance of a Solarite Component:
3366
+ * 1. new ComponentName(); // direct class instantiation
3367
+ * 2. this.html = r`<div><component-name></component-name></div>; // as a child of another RedComponent.
3368
+ * 3. <body><component-name></component-name></body> // in the Document html.
3369
+ *
3370
+ * When created via #3, Solarite has no way to pass attributes as arguments to the constructor. So to make
3371
+ * sure we get the correct value via all three paths, we write our constructors according to the following
3372
+ * example. Note that constructor args are embedded in an object, and must be all lower-case because
3373
+ * Browsers make all html attribute names lowercase.
3374
+ *
3375
+ * @example
3376
+ * constructor({name, userid=1}={}) {
3377
+ * super();
3378
+ *
3379
+ * // Get value from "name" attriute if persent, otherwise from name constructor arg.
3380
+ * this.name = getArg(this, 'name', name);
3381
+ *
3382
+ * // Optionally convert the value to an integer.
3383
+ * this.userId = getArg(this, 'userid', userid, ArgType.Int);
3384
+ * }
3385
+ *
3386
+ * @param el {HTMLElement}
3387
+ * @param attributeName {string} Attribute name. Not case-sensitive.
3388
+ * @param defaultValue {*} Default value to use if attribute doesn't exist.
3389
+ * @param type {ArgType|function|Class|*[]}
3390
+ * If an array, use the value if it's in the array, otherwise return undefined.
3391
+ * If it's a function, pass the value to the function and return the result.
3392
+ * @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
3393
+ * TODO: Should this be merged with the defaultValue argument?
3394
+ * @return {*} Undefined if attribute isn't set. */
3395
+ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String, fallback=undefined) {
3396
+ let val = defaultValue;
3397
+ let attrVal = el.getAttribute(attributeName) || el.getAttribute(Util.camelToDashes(attributeName));
3398
+ if (attrVal !== null) // If attribute doesn't exist.
3399
+ val = attrVal;
3400
+
3401
+ if (Array.isArray(type))
3402
+ return type.includes(val) ? val : fallback;
3403
+
3404
+ if (typeof type === 'function') {
3405
+ return type.constructor
3406
+ ? new type(val) // arg type is custom Class
3407
+ : type(val); // arg type is custom function
3408
+ }
3409
+
3410
+ // If bool, it's true as long as it exists and its value isn't falsey.
3411
+ if (type===ArgType.Bool) {
3412
+ let lAttrVal = typeof val === 'string' ? val.toLowerCase() : val;
3413
+ if (['false', '0', false, 0, null, undefined, NaN].includes(lAttrVal))
3414
+ return false;
3415
+ if (['true', true].includes(lAttrVal) || parseFloat(lAttrVal) !== 0)
3416
+ return true;
3417
+ return fallback;
3418
+ }
3419
+
3420
+ // Attribute doesn't exist
3421
+ let result;
3422
+ switch (type) {
3423
+ case ArgType.Int:
3424
+ result = parseInt(val);
3425
+ return isNaN(result) ? fallback : result;
3426
+ case ArgType.Float:
3427
+ result = parseFloat(val);
3428
+ return isNaN(result) ? fallback : result;
3429
+ case ArgType.String:
3430
+ return [undefined, null, false].includes(val) ? '' : val+'';
3431
+ case ArgType.Json:
3432
+ case ArgType.Eval:
3433
+ if (typeof val === 'string' && val.length)
3434
+ try {
3435
+ if (type === ArgType.Json)
3436
+ return JSON.parse(val);
3437
+ else
3438
+ return eval(`(${val})`);
3439
+ } catch (e) {
3440
+ return val;
3441
+ }
3442
+ else return val;
3443
+
3444
+ // type not provided
3445
+ default:
3446
+ return val;
3447
+ }
3448
+ }
3449
+
3450
+
3451
+ /**
3452
+ * Experimental. Set multiple arguments/attributes all at once.
3453
+ * @param el {HTMLElement}
3454
+ * @param args {Record<string, any>}
3455
+ * @param types {Record<string, ArgType|function|Class>}
3456
+ *
3457
+ * @example
3458
+ * constructor({user, path}={}) {
3459
+ * setArgs(this, arguments[0], {user: User, path: ArgType.String});
3460
+ * }
3461
+ */
3462
+ function setArgs(el, args, types) {
3463
+ for (let name in args)
3464
+ this[name] = getArg(el, name, args[name], types[name] || ArgType.String);
3465
+ }
3466
+
3467
+
3468
+ /**
3469
+ * @enum */
3470
+ var ArgType = {
3471
+
3472
+ /**
3473
+ * false, 0, null, undefined, '0', and 'false' (case-insensitive) become false.
3474
+ * Anything else, including empty string becomes true.
3475
+ * Empty string is true because attributes with no value should be evaulated as true. */
3476
+ Bool: 'Bool',
3477
+
3478
+ Int: 'Int',
3479
+ Float: 'Float',
3480
+ String: 'String',
3187
3481
 
3482
+ /** @deprecated for Json */
3483
+ JSON: 'Json',
3188
3484
 
3485
+ /**
3486
+ * Parse the string value as JSON.
3487
+ * If it's not parsable, return the value as a string. */
3488
+ Json: 'Json',
3189
3489
 
3490
+ /**
3491
+ * Evaluate the string as JavaScript using the eval() function.
3492
+ * If it can't be evaluated, return the original string. */
3493
+ Eval: 'Eval'
3494
+ };
3495
+
3190
3496
  function defineClass(Class, tagName, extendsTag) {
3191
- if (!customElements.getName(Class)) { // If not previously defined.
3192
- tagName = tagName || camelToDashes(Class.name);
3497
+ if (!customElements[getName](Class)) { // If not previously defined.
3498
+ tagName = tagName || Util.camelToDashes(Class.name);
3193
3499
  if (!tagName.includes('-'))
3194
3500
  tagName += '-element';
3195
3501
 
@@ -3197,21 +3503,17 @@ function defineClass(Class, tagName, extendsTag) {
3197
3503
  if (extendsTag)
3198
3504
  options = {extends: extendsTag};
3199
3505
 
3200
- customElements.define(tagName, Class, options);
3506
+ customElements[define](tagName, Class, options);
3201
3507
  }
3202
3508
  }
3203
3509
 
3204
-
3205
-
3206
-
3207
-
3208
3510
  /**
3209
3511
  * Create a version of the Solarite class that extends from the given tag name.
3210
3512
  * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
3211
3513
  * 1. customElements.define() is called automatically when you create the first instance.
3212
3514
  * 2. Calls render() when added to the DOM, if it hasn't been called already.
3213
- * 3. Child elements are added before constructor is called. But they're also passed to the constructor.
3214
- * 4. We can use this.html = r`...` to set html.
3515
+ * 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
3516
+ * 4. We can use this.html = r`...` to set html. (deprecated)
3215
3517
  * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
3216
3518
  * Can't figure out how to have these work standalone though, and still be synchronous.
3217
3519
  * 6. Can we extend from other element types like TR?
@@ -3230,10 +3532,10 @@ function createSolarite(extendsTag=null) {
3230
3532
  if (extendsTag && !extendsTag.includes('-')) {
3231
3533
  extendsTag = extendsTag.toLowerCase();
3232
3534
 
3233
- BaseClass = Globals.elementClasses[extendsTag];
3535
+ BaseClass = Globals$1.elementClasses[extendsTag];
3234
3536
  if (!BaseClass) { // TODO: Use Cache
3235
3537
  BaseClass = document.createElement(extendsTag).constructor;
3236
- Globals.elementClasses[extendsTag] = BaseClass;
3538
+ Globals$1.elementClasses[extendsTag] = BaseClass;
3237
3539
  }
3238
3540
  }
3239
3541
 
@@ -3259,10 +3561,10 @@ function createSolarite(extendsTag=null) {
3259
3561
  * TODO: Make these standalone functions.
3260
3562
  * Callbacks.
3261
3563
  * Use onConnect.push(() => ...); to add new callbacks. */
3262
- onConnect = Util$1.callback();
3564
+ onConnect;
3263
3565
 
3264
- onFirstConnect = Util$1.callback();
3265
- onDisconnect = Util$1.callback();
3566
+ onFirstConnect;
3567
+ onDisconnect;
3266
3568
 
3267
3569
  /**
3268
3570
  * @param options {RenderOptions} */
@@ -3274,27 +3576,28 @@ function createSolarite(extendsTag=null) {
3274
3576
  this.render();
3275
3577
 
3276
3578
  else if (options.render===false)
3277
- Globals.rendered.add(this); // Don't render on connectedCallback()
3579
+ Globals$1.rendered.add(this); // Don't render on connectedCallback()
3278
3580
 
3279
- // Add children before constructor code executes.
3280
- // PendingChildren is setup in NodeGroup.createNewComponent()
3581
+ // Add slot children before constructor code executes.
3582
+ // This breaks the styleStaticNested test.
3583
+ // PendingChildren is setup in NodeGroup.instantiateComponent()
3281
3584
  // TODO: Match named slots.
3282
- let ch = Globals.pendingChildren.pop();
3283
- if (ch)
3284
- (this.querySelector('slot') || this).append(...ch);
3585
+ //let ch = Globals.pendingChildren.pop();
3586
+ //if (ch) // TODO: how could there be a slot before render is called?
3587
+ // (this.querySelector('slot') || this).append(...ch);
3285
3588
 
3286
- /** @deprecated */
3589
+ /** @deprecated
3287
3590
  Object.defineProperty(this, 'html', {
3288
3591
  set(html) {
3289
3592
  Globals.rendered.add(this);
3290
3593
  if (typeof html === 'string') {
3291
- console.warn("Assigning to this.html without the r template prefix.");
3594
+ console.warn("Assigning to this.html without the r template prefix.")
3292
3595
  this.innerHTML = html;
3293
3596
  }
3294
3597
  else
3295
3598
  this.modifications = r(this, html, options);
3296
3599
  }
3297
- });
3600
+ })*/
3298
3601
 
3299
3602
  /*
3300
3603
  let pthis = new Proxy(this, {
@@ -3309,7 +3612,7 @@ function createSolarite(extendsTag=null) {
3309
3612
  /**
3310
3613
  * Call render() only if it hasn't already been called. */
3311
3614
  renderFirstTime() {
3312
- if (!Globals.rendered.has(this) && this.render)
3615
+ if (!Globals$1.rendered.has(this) && this.render)
3313
3616
  this.render();
3314
3617
  }
3315
3618
 
@@ -3317,279 +3620,30 @@ function createSolarite(extendsTag=null) {
3317
3620
  * Called automatically by the browser. */
3318
3621
  connectedCallback() {
3319
3622
  this.renderFirstTime();
3320
- if (!Globals.connected.has(this)) {
3321
- Globals.connected.add(this);
3322
- this.onFirstConnect();
3623
+ if (!Globals$1.connected.has(this)) {
3624
+ Globals$1.connected.add(this);
3625
+ if (this.onFirstConnect)
3626
+ this.onFirstConnect();
3323
3627
  }
3324
- this.onConnect();
3628
+ if (this.onConnect)
3629
+ this.onConnect();
3325
3630
  }
3326
3631
 
3327
3632
  disconnectedCallback() {
3328
- this.onDisconnect();
3633
+ if (this.onDisconnect)
3634
+ this.onDisconnect();
3329
3635
  }
3330
3636
 
3331
3637
 
3332
3638
  static define(tagName=null) {
3333
3639
  defineClass(this, tagName, extendsTag);
3334
3640
  }
3335
-
3336
- //#IFDEV
3337
-
3338
- /** @deprecated */
3339
- renderWatched() {
3340
- let ngm = NodeGroupManager.get(this);
3341
-
3342
- let nodeGroupUpdates = [];
3343
-
3344
- for (let change of ngm.changes) {
3345
- if (change.action === 'set') {
3346
- for (let transformerInfo of change.transformerInfo) {
3347
-
3348
- let oldHash = transformerInfo.hash;
3349
-
3350
- let newObj = delve(watchSet(transformerInfo.path[0]), transformerInfo.path.slice(1));
3351
- let newTemplate = transformerInfo.transformer(newObj);
3352
- let newHash = getObjectHash(newTemplate);
3353
- let ngs = [...ngm.nodeGroupsAvailable.data[oldHash]];
3354
- for (let ng of ngs) {
3355
- nodeGroupUpdates.push([ng, oldHash, newHash, newTemplate.exprs, transformerInfo]);
3356
- }
3357
- }
3358
- }
3359
-
3360
- else if (change.action === 'delete') {
3361
- for (let hash of change.value) {
3362
- let ngs = [...ngm.nodeGroupsAvailable.getAll(hash)]; // deletes from nodeGroupsAvailable.
3363
-
3364
- for (let ng of ngs) {
3365
- if (ng.parentPath)
3366
- ng.parentPath.clearNodesCache();
3367
-
3368
- for (let node of ng.getNodes())
3369
- node.remove();
3370
-
3371
- // TODO: Update ancestor NodeGroup exactKeys
3372
- }
3373
- }
3374
- }
3375
- else if (change.action === 'insert') {
3376
-
3377
- let beforeNg = change.beforeTemplate ? ngm.getNodeGroup(change.beforeTemplate, true) : null;
3378
- let arrayPath = [change.root, ...change.path];
3379
-
3380
- // Get anchor so we can use it to get the parent
3381
- // TODO: Should this be watchGet(change.root) ?
3382
- for (let loopInfo of ngm.getLoopInfo([change.root, ...change.path.slice(0, -1)])) {
3383
-
3384
- // Change.extra is aTemplate telling us where to insert before.
3385
- let beforeNode = beforeNg?.startNode || loopInfo.template.parentPath.nodeMarker;
3386
-
3387
- // Loop over every item added to the array.
3388
- let i = 0; // TODO: How to get real insert index.
3389
- for (let obj of change.value) {
3390
-
3391
- // Same logic as forEach() function.
3392
-
3393
- let callback = loopInfo.itemTransformer;
3394
- let path = [...arrayPath.slice(0, -1), (arrayPath.at(-1) * 1 + i) + ''];
3395
-
3396
- // Shortened logic found in watchGet(), but not any faster?
3397
- // the watchSet() is what makes this slower!
3398
- // let obj = delve(watchSet(path[0]), path.slice(1));
3399
- // let template = callback(obj);
3400
- // let serializedPath = serializePath(path);
3401
- // pathToTransformer.add(serializedPath, new TransformerInfo(path, callback, template)); // Uses a Set() to ensure no duplicates.
3402
-
3403
- let template = watchGet(path, callback);
3404
- i++;
3405
-
3406
-
3407
- //let template = loopInfo.itemTransformer(obj); // What if it takes more than one obj argument?
3408
-
3409
- // Create new NodeGroup
3410
- let ng = ngm.getNodeGroup(template, false, true);
3411
- ng.parentPath = beforeNg?.parentPath || loopInfo.template.parentPath;
3412
-
3413
- for (let node of ng.getNodes())
3414
- beforeNode.parentNode.insertBefore(node, beforeNode);
3415
-
3416
- if (ng.parentPath) // This check is needed for the forEachSpliceInsert test, but why?
3417
- ng.parentPath.clearNodesCache();
3418
- }
3419
-
3420
- // TODO: Update ancestor NodeGroup exactKeys
3421
- }
3422
- }
3423
- }
3424
-
3425
- // Update them all at once, that way we can reassign the same value twice.
3426
- for (let [ng, oldHash, newHash, exprs, ti] of nodeGroupUpdates) {
3427
- ng.applyExprs(exprs);
3428
- ngm.nodeGroupsAvailable.data[oldHash].delete(ng);
3429
- ng.exactKey = ti.hash = newHash;
3430
- ngm.nodeGroupsAvailable.add(ng.exactKey, ng); // Add back to Map with new key.
3431
- }
3432
-
3433
-
3434
- ngm.changes = [];
3435
-
3436
- return []; // TODO
3437
- }
3438
-
3439
- /**
3440
- * @deprecated Use the getArg() function instead. */
3441
- getArg(name, val=null, type=ArgType.String) {
3442
- throw new Error('deprecated');
3443
- }
3444
- //#ENDIF
3445
3641
  }
3446
- }
3447
-
3448
- /**
3449
- * Trying to be able to automatically watch primitive values.
3450
- * TODO:
3451
- * 1. Have get() return Proxies for nested updates.
3452
- * 2. Override .map() for loops to capture changes.
3453
- */
3454
-
3455
- let unusedArg = Symbol('unusedArg');
3456
-
3457
- /**
3458
- * Custom map function triggers the get() Proxy.
3459
- * @param array {Array}
3460
- * @param callback {function}
3461
- * @returns {*[]} */
3462
- function map(array, callback) {
3463
- let result = [];
3464
- for (let i=0; i<array.length; i++)
3465
- result.push(callback(array[i], i, array));
3466
- return result;
3467
- }
3468
-
3469
-
3470
- /**
3471
- *
3472
- * @param root {HTMLElement}
3473
- * @param field {string}
3474
- * @param value {string|Symbol} */
3475
- function watch3(root, field, value=unusedArg) {
3476
- // Store internal value used by get/set.
3477
- if (value !== unusedArg)
3478
- root[field] = value;
3479
- else
3480
- value = root[field];
3481
-
3482
-
3483
- // use a single object for both defineProperty and new Proxy's handler.
3484
- const handler = {
3485
- get(obj, prop, receiver) {
3486
-
3487
- let result = (obj === receiver && field === prop)
3488
- ? value // top-level value.
3489
- : Reflect.get(obj, prop, receiver); // avoid infinite recursion.
3490
-
3491
- if (prop === 'map')
3492
-
3493
- // Double function so the ExprPath calls it as a function,
3494
- // instead of it being evaluated immediately when the Templat eis created.
3495
- return (callback) => () => {
3496
- let rootNg = Globals.nodeGroups.get(root);
3497
- rootNg.mapCallbacks.set(obj, callback);
3498
- return map(new Proxy(obj, handler), callback);
3499
- }
3500
-
3501
- // Track which ExprPath is using this variable.
3502
- if (Globals.currentExprPath) {
3503
- let [exprPath, exprFunction] = Globals.currentExprPath; // Set in ExprPath.applyExact()
3504
-
3505
- let rootNg = Globals.nodeGroups.get(root);
3506
-
3507
- // Init for field.
3508
- rootNg.watchedExprPaths[field] = rootNg.watchedExprPaths[field] || new Set();
3509
- rootNg.watchedExprPaths[field].add(exprPath);
3510
- }
3511
-
3512
- if (result && typeof result === 'object')
3513
- return new Proxy(result, handler);
3514
-
3515
- return result;
3516
- },
3517
-
3518
-
3519
- // TODO: Will fail for attribute w/ a value having multiple ExprPaths.
3520
- // TODO: This won't update a component's expressions.
3521
- set(obj, prop, val, receiver) {
3522
-
3523
- // 1. Set the value.
3524
- if (obj === receiver && field === prop)
3525
- value = val; // top-level value.
3526
- else // avoid infinite recursion.
3527
- Reflect.set(obj, prop, val, receiver);
3528
-
3529
- // 2. Add to the list of ExprPaths to re-render.
3530
- let rootNg = Globals.nodeGroups.get(root);
3531
- for (let exprPath of rootNg.watchedExprPaths[field]) {
3532
-
3533
- // Update a single NodeGroup created by array.map()
3534
- if (Array.isArray(obj) && parseInt(prop) == prop) {
3535
- let exprsToRender = rootNg.exprsToRender.get(exprPath);
3536
-
3537
- // If we're not re-rendering the whole thing.
3538
- if (exprsToRender !== true)
3539
- Util$1.mapAdd(rootNg.exprsToRender, exprPath, [obj, prop, val]);
3540
- }
3541
-
3542
- // Reapply the whole expression.
3543
- else
3544
- rootNg.exprsToRender.set(exprPath, true);
3545
- }
3546
- return true;
3547
- }
3548
- };
3549
-
3550
- Object.defineProperty(root, field, {
3551
- get: () => handler.get(root, field, root),
3552
- set: (val) => handler.set(root, field, val, root)
3553
- });
3554
3642
  }
3555
3643
 
3556
- /**
3557
- * TODO: Rename so we have watch.add() and watch.render() ?
3558
- * @param root
3559
- * @returns {*[]} */
3560
- function renderWatched(root) {
3561
- let rootNg = Globals.nodeGroups.get(root);
3562
- let modified = [];
3563
-
3564
- for (let [exprPath, params] of rootNg.exprsToRender) {
3565
-
3566
- // Reapply the whole expression.
3567
- if (params === true) {
3568
- exprPath.apply(exprPath.watchFunction);
3569
-
3570
- // TODO: freeNodeGroups() could be skipped if applyExprs() never marked them as in-use.
3571
- exprPath.freeNodeGroups();
3572
-
3573
- modified.push(...exprPath.getNodes());
3574
- }
3575
-
3576
- // Update a single NodeGroup created by array.map()
3577
- else {
3578
- for (let row of params) {
3579
- let [obj, prop, value] = row;
3580
- let callback = rootNg.mapCallbacks.get(obj);
3581
- let template = callback(value);
3582
- exprPath.applyLoopItemUpdate(prop, template);
3583
-
3584
- modified.push(...exprPath.nodeGroups[prop].getNodes());
3585
- }
3586
- }
3587
- }
3588
-
3589
- rootNg.exprsToRender = new Map(); // clear
3590
-
3591
- return modified;
3592
- }
3644
+ // Trick to prevent minifier from renaming this method.
3645
+ let define = 'define';
3646
+ let getName = 'getName';
3593
3647
 
3594
3648
  /**
3595
3649
  * Solarite JavasCript UI library.
@@ -3600,12 +3654,13 @@ function renderWatched(root) {
3600
3654
  /**
3601
3655
  * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
3602
3656
  * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
3603
- let Solarite = new Proxy(createSolarite(), {
3657
+ const Solarite = new Proxy(createSolarite(), {
3604
3658
  apply(self, _, args) {
3605
3659
  return createSolarite(...args)
3606
3660
  }
3607
3661
  });
3608
- let getInputValue = Util.getInputValue;
3609
- // unfinished
3662
+
3663
+ //export {default as watch, renderWatched} from './watch.js'; // unfinished
3610
3664
 
3611
- export { ArgType, Globals, Solarite, Template, delve, getArg, getInputValue, r, renderWatched, watch3 as watch };
3665
+ export default h;
3666
+ export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs };