solarite 0.3.0 → 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 (34) hide show
  1. package/dist/Solarite-debug.js +380 -504
  2. package/dist/Solarite.js +363 -487
  3. package/dist/Solarite.min.js +2 -2
  4. package/package.json +5 -6
  5. package/readme.md +1 -1
  6. package/src/{solarite/ExprPath.js → ExprPath.js} +23 -49
  7. package/src/{solarite/Globals.js → Globals.js} +4 -4
  8. package/src/{util/MultiValueMap.js → MultiValueMap.js} +1 -32
  9. package/src/{solarite/NodeGroup.js → NodeGroup.js} +25 -21
  10. package/src/{solarite/Shell.js → Shell.js} +8 -7
  11. package/src/Solarite.d.ts +62 -0
  12. package/src/{solarite/Solarite.js → Solarite.js} +13 -15
  13. package/src/{solarite/Template.js → Template.js} +1 -1
  14. package/src/{solarite/Util.js → Util.js} +49 -45
  15. package/src/{solarite/createSolarite.js → createSolarite.js} +12 -10
  16. package/src/{solarite/getArg.js → getArg.js} +24 -3
  17. package/src/{solarite/h.js → h.js} +4 -12
  18. package/src/unused/FastLookupArray.js +54 -0
  19. package/src/unused/Hashes.js +339 -0
  20. package/src/unused/InUse.test.js +92 -0
  21. package/src/unused/InUseMap.js +98 -0
  22. package/src/unused/LinkedList.js +117 -0
  23. package/src/unused/LinkedList.test.js +115 -0
  24. package/src/unused/Misc.js +13 -0
  25. package/src/unused/Perf.js +47 -0
  26. package/src/unused/TrackedArray.js +54 -0
  27. package/src/unused/WeakArray.js +33 -0
  28. package/src/{solarite/watch.js → watch.js} +4 -4
  29. package/src/util/Util.js +0 -101
  30. /package/src/{solarite/HtmlParser.js → HtmlParser.js} +0 -0
  31. /package/src/{util/Errors.js → assert.js} +0 -0
  32. /package/src/{util/delve.js → delve.js} +0 -0
  33. /package/src/{solarite/hash.js → hash.js} +0 -0
  34. /package/src/{solarite/udomdiff.js → udomdiff.js} +0 -0
package/dist/Solarite.js CHANGED
@@ -1,98 +1,92 @@
1
- /**
2
- * @typedef {Array|function(...*)} Callbacks
3
- * @property {function(function)} push
4
- * @property {function()} remove
5
- * @property {function()} pause
6
- * @property {function()} resume
7
- * */
1
+
2
+
3
+ let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
4
+ let objectIds = new WeakMap();
8
5
 
6
+ /**
7
+ * @param obj {Object|string|Node}
8
+ * @returns {string} */
9
+ function getObjectId(obj) {
10
+ // if (typeof obj === 'function')
11
+ // return obj.toString(); // This fails to detect when a function's bound variables changes.
12
+
13
+ let result = objectIds.get(obj);
14
+ if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
15
+ result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
16
+ objectIds.set(obj, result);
17
+ }
18
+ return result;
19
+ }
9
20
 
10
21
  /**
11
- * A place for functions that have no other home. */
12
- var Util$1 = {
22
+ * Control how JSON.stringify() handles Nodes and Functions.
23
+ * Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
24
+ * But that makes JSON.stringify() take twice as long to run.
25
+ * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
26
+ let isHashing = true;
27
+ function toJSON() {
28
+ return isHashing ? getObjectId(this) : this
29
+ }
13
30
 
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
31
 
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;
32
+ // Node.prototype.toJSON = toJSON;
33
+ // Function.prototype.toJSON = toJSON;
34
+
70
35
 
71
- result.resume = () => paused = false;
36
+ /**
37
+ * Get a string that uniquely maps to the values of the given object.
38
+ * If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
39
+ * This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
40
+ *
41
+ * Relies on the Node and Function prototypes being overridden above.
42
+ *
43
+ * Note that passing an integer may collide with the number we get from hashing an object.
44
+ * But we don't handle that case because we need max performance and Solarite never passes integers to this function.
45
+ *
46
+ * @param obj {*}
47
+ * @returns {string} */
48
+ function getObjectHash(obj) {
72
49
 
73
- // Add initial functions
74
- for (let f of functions)
75
- result.push(f);
50
+ // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
51
+ // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
52
+ // So we check the assignments on every run of getObjectHash()
53
+ if (Node.prototype.toJSON !== toJSON) {
54
+ Node.prototype.toJSON = toJSON;
55
+ if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
56
+ Function.prototype.toJSON = toJSON;
57
+ }
76
58
 
77
- return result;
78
- },
59
+ let result;
60
+ isHashing = true;
61
+ try {
62
+ result = JSON.stringify(obj);
63
+ }
64
+ catch(e) {
65
+ result = getObjectHashCircular(obj);
66
+ }
67
+ isHashing = false;
68
+ return result;
69
+ }
79
70
 
80
- /**
81
- * Use an array as the value of a map, appending to it when we add.
82
- * Used by watch.js.
83
- * @param map {Map|WeakMap|Object}
84
- * @param key
85
- * @param value */
86
- mapArrayAdd(map, key, value) {
87
- let result = map.get(key);
88
- if (!result) {
89
- result = [value];
90
- map.set(key, result);
71
+ /**
72
+ * Slower hashing method that supports.
73
+ * @param obj
74
+ * @returns {string} */
75
+ function getObjectHashCircular(obj) {
76
+
77
+ //console.log('circular')
78
+ // Slower version that handles circular references.
79
+ // Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
80
+ const seen = new Set();
81
+ return JSON.stringify(obj, (key, value) => {
82
+ if (typeof value === 'object' && value !== null) {
83
+ if (seen.has(value))
84
+ return getObjectId(value);
85
+ seen.add(value);
91
86
  }
92
- else
93
- result.push(value);
94
- },
95
- };
87
+ return value;
88
+ });
89
+ }
96
90
 
97
91
  var Globals;
98
92
 
@@ -119,15 +113,15 @@ function reset() {
119
113
  div: document.createElement("div"),
120
114
 
121
115
  /**
122
- * @type {Object<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
116
+ * @type {Record<string, Class<Node>>} A map from built-in tag names to the constructors that create them. */
123
117
  elementClasses: {},
124
118
 
125
- /** @type {Object<string, boolean>} Key is tag-name.propName. Value is whether it's an attribute.*/
119
+ /** @type {Record<string, boolean>} Key is tag-name.propName. Value is whether it's an attribute.*/
126
120
  htmlProps: {},
127
121
 
128
122
  /**
129
123
  * Used by ExprPath.applyEventAttrib()
130
- * @type {WeakMap<Node, Object<eventName:string, [original:function, bound:function, args:*[]]>>} */
124
+ * @type {WeakMap<Node, Record<eventName:string, [original:function, bound:function, args:*[]]>>} */
131
125
  nodeEvents: new WeakMap(),
132
126
 
133
127
  /**
@@ -162,7 +156,7 @@ function reset() {
162
156
  * A map of individual untagged strings to their Templates.
163
157
  * This way we don't keep creating new Templates for the same string when re-rendering.
164
158
  * This is used by ExprPath.applyExactNodes()
165
- * @type {Object<string, Template>} */
159
+ * @type {Record<string, Template>} */
166
160
  //stringTemplates: {},
167
161
 
168
162
  reset,
@@ -221,6 +215,21 @@ let d = {};
221
215
 
222
216
  let Util = {
223
217
 
218
+ /**
219
+ * Returns true if they're the same.
220
+ * @param a
221
+ * @param b
222
+ * @returns {boolean} */
223
+ arraySame(a, b) {
224
+ let aLength = a.length;
225
+ if (aLength !== b.length)
226
+ return false;
227
+ for (let i=0; i<aLength; i++)
228
+ if (a[i] !== b[i])
229
+ return false;
230
+ return true; // the same.
231
+ },
232
+
224
233
  bindId(root, el) {
225
234
  let id = el.getAttribute('data-id') || el.getAttribute('id');
226
235
  if (id) { // If something hasn't removed the id.
@@ -261,7 +270,6 @@ let Util = {
261
270
  }
262
271
  },
263
272
 
264
-
265
273
  /**
266
274
  * Convert a Proper Case name to a name with dashes.
267
275
  * Dashes will be placed between letters and numbers.
@@ -329,17 +337,17 @@ let Util = {
329
337
  * for (const item of flatten(complexArray))
330
338
  * console.log(item); // Outputs: 1, 2, 3, 4, 5, 6, { a: 'object' }, 7, 8, 9
331
339
  */
332
- *flatten(value) {
333
- if (Array.isArray(value)) {
334
- for (const item of value) {
335
- yield* Util.flatten(item); // Recursively flatten arrays
336
- }
337
- } else if (typeof value === 'function') {
338
- const result = value();
339
- yield* Util.flatten(result); // Recursively flatten the result of a function
340
- } else
341
- yield value; // Yield primitive values as is
342
- },
340
+ // *flatten(value) {
341
+ // if (Array.isArray(value)) {
342
+ // for (const item of value) {
343
+ // yield* Util.flatten(item); // Recursively flatten arrays
344
+ // }
345
+ // } else if (typeof value === 'function') {
346
+ // const result = value();
347
+ // yield* Util.flatten(result); // Recursively flatten the result of a function
348
+ // } else
349
+ // yield value; // Yield primitive values as is
350
+ // },
343
351
 
344
352
  /**
345
353
  * Get the value of an input as the most appropriate JavaScript type.
@@ -361,6 +369,10 @@ let Util = {
361
369
  return node.value; // String
362
370
  },
363
371
 
372
+ isEvent(attrName) {
373
+ return attrName.startsWith('on') && attrName in Globals$1.div;
374
+ },
375
+
364
376
  /**
365
377
  * @param el {HTMLElement}
366
378
  * @param prop {string}
@@ -400,9 +412,10 @@ let Util = {
400
412
  return val === undefined || val === false || val === null;
401
413
  },
402
414
 
415
+ /*
403
416
  isPrimitive(val) {
404
417
  return typeof val === 'string' || typeof val === 'number'
405
- },
418
+ },*/
406
419
 
407
420
  /**
408
421
  * If val is a function, evaluate it recursively until the result is not a function.
@@ -420,6 +433,22 @@ let Util = {
420
433
  return val;
421
434
  },
422
435
 
436
+ /**
437
+ * Use an array as the value of a map, appending to it when we add.
438
+ * Used by watch.js.
439
+ * @param map {Map|WeakMap|Object}
440
+ * @param key
441
+ * @param value */
442
+ mapArrayAdd(map, key, value) {
443
+ let result = map.get(key);
444
+ if (!result) {
445
+ result = [value];
446
+ map.set(key, result);
447
+ }
448
+ else
449
+ result.push(value);
450
+ },
451
+
423
452
  /**
424
453
  * Remove nodes from the beginning and end that are not:
425
454
  * 1. Elements.
@@ -442,282 +471,18 @@ let Util = {
442
471
  while (result.length > 0 && shouldTrimNode(result[result.length - 1]))
443
472
  result.pop();
444
473
 
445
- return result;
446
- }
447
- };
448
-
449
-
450
-
451
- let isEvent = attrName => attrName.startsWith('on') && attrName in Globals$1.div;
452
-
453
-
454
-
455
-
456
-
457
- /**
458
- * Returns true if they're the same.
459
- * @param a
460
- * @param b
461
- * @returns {boolean} */
462
- function arraySame(a, b) {
463
- let aLength = a.length;
464
- if (aLength !== b.length)
465
- return false;
466
- for (let i=0; i<aLength; i++)
467
- if (a[i] !== b[i])
468
- return false;
469
- return true; // the same.
470
- }
471
-
472
-
473
-
474
-
475
-
476
- // For debugging only
477
-
478
-
479
- function defineClass(Class, tagName, extendsTag) {
480
- if (!customElements[getName](Class)) { // If not previously defined.
481
- tagName = tagName || Util.camelToDashes(Class.name);
482
- if (!tagName.includes('-'))
483
- tagName += '-element';
484
-
485
- let options = null;
486
- if (extendsTag)
487
- options = {extends: extendsTag};
488
-
489
- customElements[define](tagName, Class, options);
490
- }
491
- }
492
-
493
- /**
494
- * Create a version of the Solarite class that extends from the given tag name.
495
- * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
496
- * 1. customElements.define() is called automatically when you create the first instance.
497
- * 2. Calls render() when added to the DOM, if it hasn't been called already.
498
- * 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
499
- * 4. We can use this.html = r`...` to set html. (deprecated)
500
- * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
501
- * Can't figure out how to have these work standalone though, and still be synchronous.
502
- * 6. Can we extend from other element types like TR?
503
- * 7. Shows default text if render() function isn't defined.
504
- *
505
- * Advantages to inheriting from HTMLElement
506
- * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
507
- * 2. We can inherit from things like HTMLTableRowElement directly.
508
- * 3. There's less magic, since everyone is familiar with defining custom elements.
509
- *
510
- * @param extendsTag {?string}
511
- * @return {Class} */
512
- function createSolarite(extendsTag=null) {
513
-
514
- let BaseClass = HTMLElement;
515
- if (extendsTag && !extendsTag.includes('-')) {
516
- extendsTag = extendsTag.toLowerCase();
517
-
518
- BaseClass = Globals$1.elementClasses[extendsTag];
519
- if (!BaseClass) { // TODO: Use Cache
520
- BaseClass = document.createElement(extendsTag).constructor;
521
- Globals$1.elementClasses[extendsTag] = BaseClass;
522
- }
523
- }
524
-
525
- /**
526
- * Intercept the construct call to auto-define the class before the constructor is called.
527
- * @type {HTMLElement} */
528
- let HTMLElementAutoDefine = new Proxy(BaseClass, {
529
- construct(Parent, args, Class) {
530
- defineClass(Class, null, extendsTag);
531
-
532
- // This is a good place to manipulate any args before they're sent to the constructor.
533
- // Such as loading them from attributes, if I could find a way to do so.
534
-
535
- // This line is equivalent the to super() call.
536
- return Reflect.construct(Parent, args, Class);
537
- }
538
- });
539
-
540
- return class Solarite extends HTMLElementAutoDefine {
541
-
542
-
543
- /**
544
- * TODO: Make these standalone functions.
545
- * Callbacks.
546
- * Use onConnect.push(() => ...); to add new callbacks. */
547
- onConnect = Util$1.callback();
548
-
549
- onFirstConnect = Util$1.callback();
550
- onDisconnect = Util$1.callback();
551
-
552
- /**
553
- * @param options {RenderOptions} */
554
- constructor(options={}) {
555
- super();
556
-
557
- // TODO: Is options.render ever used?
558
- if (options.render===true)
559
- this.render();
560
-
561
- else if (options.render===false)
562
- Globals$1.rendered.add(this); // Don't render on connectedCallback()
563
-
564
- // Add slot children before constructor code executes.
565
- // This breaks the styleStaticNested test.
566
- // PendingChildren is setup in NodeGroup.createNewComponent()
567
- // TODO: Match named slots.
568
- //let ch = Globals.pendingChildren.pop();
569
- //if (ch) // TODO: how could there be a slot before render is called?
570
- // (this.querySelector('slot') || this).append(...ch);
571
-
572
- /** @deprecated
573
- Object.defineProperty(this, 'html', {
574
- set(html) {
575
- Globals.rendered.add(this);
576
- if (typeof html === 'string') {
577
- console.warn("Assigning to this.html without the r template prefix.")
578
- this.innerHTML = html;
579
- }
580
- else
581
- this.modifications = r(this, html, options);
582
- }
583
- })*/
584
-
585
- /*
586
- let pthis = new Proxy(this, {
587
- get(obj, prop) {
588
- return Reflect.get(obj, prop)
589
- }
590
- });
591
- this.render = this.render.bind(pthis);
592
- */
593
- }
594
-
595
- /**
596
- * Call render() only if it hasn't already been called. */
597
- renderFirstTime() {
598
- if (!Globals$1.rendered.has(this) && this.render)
599
- this.render();
600
- }
601
-
602
- /**
603
- * Called automatically by the browser. */
604
- connectedCallback() {
605
- this.renderFirstTime();
606
- if (!Globals$1.connected.has(this)) {
607
- Globals$1.connected.add(this);
608
- this.onFirstConnect();
609
- }
610
- this.onConnect();
611
- }
612
-
613
- disconnectedCallback() {
614
- this.onDisconnect();
615
- }
616
-
617
-
618
- static define(tagName=null) {
619
- defineClass(this, tagName, extendsTag);
620
- }
621
- }
622
- }
623
-
624
- // Trick to prevent minifier from renaming this method.
625
- let define = 'define';
626
- let getName = 'getName';
627
-
628
-
629
-
630
- let lastObjectId = 1>>>0; // Is a 32-bit int faster to increment than JavaScript's Number, which is a 64-bit float?
631
- let objectIds = new WeakMap();
632
-
633
- /**
634
- * @param obj {Object|string|Node}
635
- * @returns {string} */
636
- function getObjectId(obj) {
637
- // if (typeof obj === 'function')
638
- // return obj.toString(); // This fails to detect when a function's bound variables changes.
639
-
640
- let result = objectIds.get(obj);
641
- if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
642
- result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
643
- objectIds.set(obj, result);
644
- }
645
- return result;
646
- }
647
-
648
- /**
649
- * Control how JSON.stringify() handles Nodes and Functions.
650
- * Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
651
- * But that makes JSON.stringify() take twice as long to run.
652
- * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
653
- let isHashing = true;
654
- function toJSON() {
655
- return isHashing ? getObjectId(this) : this
656
- }
657
-
658
-
659
- // Node.prototype.toJSON = toJSON;
660
- // Function.prototype.toJSON = toJSON;
661
-
662
-
663
- /**
664
- * Get a string that uniquely maps to the values of the given object.
665
- * If a value in obj changes, calling getObjectHash(obj) will then return a different hash.
666
- * This is used by NodeGroupManager to create a hash that represents the current values of a NodeGroup.
667
- *
668
- * Relies on the Node and Function prototypes being overridden above.
669
- *
670
- * Note that passing an integer may collide with the number we get from hashing an object.
671
- * But we don't handle that case because we need max performance and Solarite never passes integers to this function.
672
- *
673
- * @param obj {*}
674
- * @returns {string} */
675
- function getObjectHash(obj) {
676
-
677
- // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
678
- // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
679
- // So we check the assignments on every run of getObjectHash()
680
- if (Node.prototype.toJSON !== toJSON) {
681
- Node.prototype.toJSON = toJSON;
682
- if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
683
- Function.prototype.toJSON = toJSON;
474
+ return result;
684
475
  }
476
+ };
685
477
 
686
- let result;
687
- isHashing = true;
688
- try {
689
- result = JSON.stringify(obj);
690
- }
691
- catch(e) {
692
- result = getObjectHashCircular(obj);
693
- }
694
- isHashing = false;
695
- return result;
696
- }
697
478
 
698
- /**
699
- * Slower hashing method that supports.
700
- * @param obj
701
- * @returns {string} */
702
- function getObjectHashCircular(obj) {
703
479
 
704
- //console.log('circular')
705
- // Slower version that handles circular references.
706
- // Just adding any callback at all, even one that just returns the value, makes JSON.stringify() twice as slow.
707
- const seen = new Set();
708
- return JSON.stringify(obj, (key, value) => {
709
- if (typeof value === 'object' && value !== null) {
710
- if (seen.has(value))
711
- return getObjectId(value);
712
- seen.add(value);
713
- }
714
- return value;
715
- });
716
- }
480
+ // For debugging only
481
+
717
482
 
718
483
  class MultiValueMap {
719
484
 
720
- /** @type {Object<string, Set>} */
485
+ /** @type {Record<string, Set>} */
721
486
  data = {};
722
487
 
723
488
  // Set a new value for a key
@@ -811,37 +576,6 @@ class MultiValueMap {
811
576
  return result;
812
577
  }
813
578
 
814
-
815
- /**
816
- * Try to delete an item that matches the key and the isPreferred function.
817
- * if not the latter, just delete any item that matches the key.
818
- * @param key {string}
819
- * @returns {*|undefined} The deleted item. */
820
- deletePreferred(key, parent) {
821
- let result;
822
- let data = this.data;
823
- let set = data[key];
824
- if (!set)
825
- return undefined;
826
-
827
- for (let val of set) {
828
- if (val?.parentNode === parent) {
829
- set.delete(val);
830
- result = val;
831
- break;
832
- }
833
- }
834
- if (!result) {
835
- [result] = set;
836
- set.delete(result);
837
- }
838
-
839
- if (set.size === 0)
840
- delete data[key];
841
-
842
- return result;
843
- }
844
-
845
579
  hasValue(val) {
846
580
  let data = this.data;
847
581
  let names = [];
@@ -1237,7 +971,7 @@ class ExprPath {
1237
971
 
1238
972
 
1239
973
  // This pre-check makes it a few percent faster?
1240
- let same = arraySame(oldNodes, newNodes);
974
+ let same = Util.arraySame(oldNodes, newNodes);
1241
975
  if (!same) {
1242
976
 
1243
977
  path.nodesCache = newNodes; // Replaces value set by path.getNodes()
@@ -1259,6 +993,20 @@ class ExprPath {
1259
993
  for (let ng of oldNodeGroups)
1260
994
  if (!ng.startNode.parentNode)
1261
995
  ng.removeAndSaveOrphans();
996
+
997
+
998
+
999
+
1000
+ // Instantiate components created within ${...} expressions.
1001
+ // Embedded style tags are handled elsewhere, but where?
1002
+ for (let el of newNodes) {
1003
+ if (el instanceof HTMLElement) {
1004
+ if (el.hasAttribute('solarite-placeholder'))
1005
+ this.parentNg.instantiateComponent(el);
1006
+ for (let child of el.querySelectorAll('[solarite-placeholder]'))
1007
+ this.parentNg.instantiateComponent(child);
1008
+ }
1009
+ }
1262
1010
  }
1263
1011
 
1264
1012
 
@@ -1433,50 +1181,10 @@ class ExprPath {
1433
1181
  // Arrays and functions.
1434
1182
  // I tried iterating over the result of a generator function to avoid this recursion and simplify the code,
1435
1183
  // but that consistently made the js-framework-benchmarks a few percentage points slower.
1436
- else {
1184
+ else
1437
1185
  this.exprToTemplates(expr, template => {
1438
1186
  this.applyExactNodes(template, newNodes, secondPass);
1439
1187
  });
1440
-
1441
- }
1442
-
1443
- // Old version
1444
- /*else if (Array.isArray(expr))
1445
- for (let subExpr of expr)
1446
- this.applyExactNodes(subExpr, newNodes, secondPass);
1447
-
1448
- else if (typeof expr === 'function') {
1449
- // TODO: One ExprPath can have multiple expr functions.
1450
- // But if using it as a watch, it should only have one at the top level.
1451
- // So maybe this is ok.
1452
- Globals.currentExprPath = this; // Used by watch()
1453
-
1454
- this.watchFunction = expr; // TODO: Only do this if it's a top level function.
1455
- let result = expr(); // As expr accesses watched variables, watch() uses Globals.currentExprPath to mark where those watched variables are being used.
1456
- Globals.currentExprPath = null;
1457
-
1458
- this.applyExactNodes(result, newNodes, secondPass);
1459
- }
1460
-
1461
- // String
1462
- else {
1463
- // Convert expression to a string.
1464
- let stringExpr = expr;
1465
- if (expr === undefined || expr === false || expr === null) // Util.isFalsy()
1466
- stringExpr = '';
1467
- else if (typeof expr !== 'string')
1468
- stringExpr = expr + '';
1469
-
1470
- // Get the same Template for the same string each time.
1471
- let template = Globals.stringTemplates[stringExpr];
1472
- if (!template) {
1473
- template = new Template([stringExpr], []);
1474
- Globals.stringTemplates[stringExpr] = template;
1475
- }
1476
-
1477
- // Recurse.
1478
- this.applyExactNodes(template, newNodes, secondPass);
1479
- }*/
1480
1188
  }
1481
1189
 
1482
1190
  applyMultipleAttribs(node, expr) {
@@ -1616,7 +1324,7 @@ class ExprPath {
1616
1324
  // Copies the attribute to the property when the input event fires.
1617
1325
  // value=${[this, 'value]'}
1618
1326
  // checked=${[this, 'isAgree']}
1619
- // This same logic is in NodeGroup.createNewComponent() for components.
1327
+ // This same logic is in NodeGroup.instantiateComponent() for components.
1620
1328
  if (Util.isPath(expr)) {
1621
1329
  let [obj, path] = [expr[0], expr.slice(1)];
1622
1330
 
@@ -2156,7 +1864,7 @@ class Shell {
2156
1864
  /** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
2157
1865
  staticComponents = [];
2158
1866
 
2159
- /** @type {{path:int[], attribs:Object<string, string>}[]} */
1867
+ /** @type {{path:int[], attribs:Record<string, string>}[]} */
2160
1868
  //componentAttribs = [];
2161
1869
 
2162
1870
 
@@ -2215,7 +1923,7 @@ class Shell {
2215
1923
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
2216
1924
  if (parts.length > 1) {
2217
1925
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
2218
- let type = isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
1926
+ let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
2219
1927
 
2220
1928
  this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
2221
1929
  placeholdersUsed += parts.length - 1;
@@ -2343,11 +2051,12 @@ class Shell {
2343
2051
  function addToken(token, context) {
2344
2052
 
2345
2053
  if (context === HtmlParser.Tag) {
2346
- // Find Solarite Components tags and append -solarite-placeholder to their tag names.
2054
+ // Find Solarite Components tags and append -solarite-placeholder to their tag names
2055
+ // and give them a solarite-placeholder attribute so we can easily find them later.
2347
2056
  // This way we can gather their constructor arguments and their children before we call their constructor.
2348
- // Later, NodeGroup.createNewComponent() will replace them with the real components.
2057
+ // Later, NodeGroup.instantiateComponent() will replace them with the real components.
2349
2058
  // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
2350
- token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder');
2059
+ token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder solarite-placeholder');
2351
2060
  }
2352
2061
  tokens.push(token);
2353
2062
  }
@@ -2510,7 +2219,7 @@ class NodeGroup {
2510
2219
  // Apply exprs
2511
2220
  this.applyExprs(template.exprs);
2512
2221
 
2513
- this.activateStaticComponents(staticComponents);
2222
+ this.instantiateStaticComponents(staticComponents);
2514
2223
  }
2515
2224
  else if (shell)
2516
2225
  this.activateEmbeds(fragment, shell);
@@ -2603,7 +2312,7 @@ class NodeGroup {
2603
2312
  // Think of having two adjacent components.
2604
2313
  // But the dynamicAttribsAdjacet test already passes.
2605
2314
 
2606
- // If a component:
2315
+ // If expr is an attribute in a component:
2607
2316
  // 1. Instantiate it if it hasn't already been, sending all expr's to its constructor.
2608
2317
  // 2. Otherwise send them to its render function.
2609
2318
  // Components with no expressions as attributes are instead activated in activateEmbeds().
@@ -2640,6 +2349,8 @@ class NodeGroup {
2640
2349
  // TODO: Only do this if we have ExprPaths within styles?
2641
2350
  this.updateStyles();
2642
2351
 
2352
+
2353
+
2643
2354
  // Invalidate the nodes cache because we just changed it.
2644
2355
  this.nodesCache = null;
2645
2356
 
@@ -2662,23 +2373,25 @@ class NodeGroup {
2662
2373
  // then we could re-use the hash and logic from NodeManager?
2663
2374
  let newHash = getObjectHash(props);
2664
2375
 
2665
- let isPreHtmlElement = el.tagName.endsWith('-SOLARITE-PLACEHOLDER');
2376
+ let isPreHtmlElement = el.hasAttribute('solarite-placeholder');
2666
2377
  let isPreIsElement = el.hasAttribute('_is');
2667
2378
 
2668
2379
 
2669
2380
  // Instantiate a placeholder.
2670
2381
  if (isPreHtmlElement || isPreIsElement)
2671
- el = this.createNewComponent(el, isPreHtmlElement, props);
2382
+ el = this.instantiateComponent(el, isPreHtmlElement, props);
2672
2383
 
2673
2384
  // Call render() with the same params that would've been passed to the constructor.
2385
+ // We do this even if the arguments haven't changed, so we can let the child component
2386
+ // compare the arguments and then decide for itself whether it wants to re-render.
2674
2387
  else if (el.render) {
2675
- let oldHash = Globals$1.componentArgsHash.get(el);
2676
- if (oldHash !== newHash) {
2388
+ //let oldHash = Globals.componentArgsHash.get(el);
2389
+ //if (oldHash !== newHash) { // Only if not changed.
2677
2390
  let args = {};
2678
2391
  for (let name in props || {})
2679
2392
  args[Util.dashesToCamel(name)] = props[name];
2680
2393
  el.render(args); // Pass new values of props to render so it can decide how it wants to respond.
2681
- }
2394
+ //}
2682
2395
  }
2683
2396
 
2684
2397
  Globals$1.componentArgsHash.set(el, newHash);
@@ -2691,10 +2404,10 @@ class NodeGroup {
2691
2404
  * The logic of this function is complex and could use cleaning up.
2692
2405
  *
2693
2406
  * @param el
2694
- * @param isPreHtmlElement
2407
+ * @param isPreHtmlElement {?boolean} True if the element's tag name ends with -solarite-placeholder
2695
2408
  * @param props {Object} Attributes with dynamic values.
2696
2409
  * @return {HTMLElement} */
2697
- createNewComponent(el, isPreHtmlElement=undefined, props=undefined) {
2410
+ instantiateComponent(el, isPreHtmlElement=undefined, props=undefined) {
2698
2411
  if (isPreHtmlElement === undefined)
2699
2412
  isPreHtmlElement = !el.hasAttribute('_is');
2700
2413
 
@@ -2717,7 +2430,7 @@ class NodeGroup {
2717
2430
  if (el.attributes.length) {
2718
2431
  for (let attrib of el.attributes) {
2719
2432
  let attribName = Util.dashesToCamel(attrib.name);
2720
- if (!args.hasOwnProperty(attribName))
2433
+ if (!args.hasOwnProperty(attribName) && attribName !== 'solarite-placeholder')
2721
2434
  args[attribName] = attrib.value;
2722
2435
  }
2723
2436
  }
@@ -2759,7 +2472,7 @@ class NodeGroup {
2759
2472
 
2760
2473
  // Copy attributes over.
2761
2474
  for (let attrib of el.attributes)
2762
- if (attrib.name !== '_is')
2475
+ if (attrib.name !== '_is' && attrib.name !== 'solarite-placeholder')
2763
2476
  newEl.setAttribute(attrib.name, attrib.value);
2764
2477
 
2765
2478
  // Set dynamic attributes if they are primitive types.
@@ -2854,9 +2567,9 @@ class NodeGroup {
2854
2567
  let result = [];
2855
2568
 
2856
2569
  // static components. These are WebComponents that do not have any constructor arguments that are expressions.
2857
- // Those are instead created by applyExpr() which calls applyComponentExprs() which calls createNewcomponent().
2570
+ // Those are instead created by applyExpr() which calls applyComponentExprs() which calls instantiateComponent().
2858
2571
  // Maybe someday these two paths will be merged?
2859
- // Must happen before ids because createNewComponent will replace the element.
2572
+ // Must happen before ids because instantiateComponent will replace the element.
2860
2573
  for (let path of shell.staticComponents) {
2861
2574
  if (pathOffset)
2862
2575
  path = path.slice(0, -pathOffset);
@@ -2870,13 +2583,13 @@ class NodeGroup {
2870
2583
  return result;
2871
2584
  }
2872
2585
 
2873
- activateStaticComponents(staticComponents) {
2586
+ instantiateStaticComponents(staticComponents) {
2874
2587
  for (let el of staticComponents)
2875
- this.createNewComponent(el);
2588
+ this.instantiateComponent(el);
2876
2589
  }
2877
2590
 
2878
2591
  /**
2879
- * @param root {HTMLElement}
2592
+ * @param root {HTMLElement|DocumentFragment}
2880
2593
  * @param shell {Shell}
2881
2594
  * @param pathOffset {int} */
2882
2595
  activateEmbeds(root, shell, pathOffset=0) {
@@ -2986,7 +2699,7 @@ class RootNodeGroup extends NodeGroup {
2986
2699
 
2987
2700
  // Copy attributes
2988
2701
  for (let attrib of fragment.children[0].attributes)
2989
- if (!el.hasAttribute(attrib.name))
2702
+ if (!el.hasAttribute(attrib.name) && attrib.name !== 'solarite-placeholder')
2990
2703
  el.setAttribute(attrib.name, attrib.value);
2991
2704
 
2992
2705
  // Go one level deeper into all of shell's paths.
@@ -3046,7 +2759,7 @@ class RootNodeGroup extends NodeGroup {
3046
2759
  // Apply exprs
3047
2760
  this.applyExprs(template.exprs);
3048
2761
 
3049
- this.activateStaticComponents(staticComponents);
2762
+ this.instantiateStaticComponents(staticComponents);
3050
2763
  }
3051
2764
  }
3052
2765
  }
@@ -3070,7 +2783,7 @@ function getSingleEl(fragment) {
3070
2783
  * @returns {boolean} */
3071
2784
  function isReplaceEl(fragment, el) {
3072
2785
  return fragment.children.length===1
3073
- && el.tagName.includes('-')
2786
+ && el.tagName.includes('-') // TODO: Check for solarite-placeholder attribute instead?
3074
2787
  && fragment.children[0].tagName.replace('-SOLARITE-PLACEHOLDER', '') === el.tagName;
3075
2788
  }
3076
2789
 
@@ -3233,6 +2946,8 @@ class Template {
3233
2946
  * @return {Node|HTMLElement|Template} */
3234
2947
  function h(htmlStrings=undefined, ...exprs) {
3235
2948
 
2949
+ if (htmlStrings === undefined && !exprs.length && arguments.length)
2950
+ throw new Error('h() cannot be called with undefined.');
3236
2951
 
3237
2952
  // TODO: Make this a more flat if/else and call other functions for the logic.
3238
2953
  if (htmlStrings instanceof Node) {
@@ -3255,7 +2970,7 @@ function h(htmlStrings=undefined, ...exprs) {
3255
2970
  }
3256
2971
 
3257
2972
  // 2. Render template created by #4 to element.
3258
- else if (exprs[0] instanceof Template) {
2973
+ else { // instanceof Template
3259
2974
  let options = exprs[1];
3260
2975
  template.render(parent, options);
3261
2976
 
@@ -3266,16 +2981,6 @@ function h(htmlStrings=undefined, ...exprs) {
3266
2981
  parent.append(this.rootNg.getParentNode());
3267
2982
  }
3268
2983
  }
3269
-
3270
-
3271
-
3272
- // null for expr[0], remove whole element.
3273
- // This path never happens?
3274
- else {
3275
- throw new Error('unsupported');
3276
- //let ngm = NodeGroupManager.get(parent);
3277
- //ngm.render(null, exprs[1])
3278
- }
3279
2984
  }
3280
2985
 
3281
2986
  // 3. Path if used as a template tag.
@@ -3412,7 +3117,7 @@ let renderF = 'render';
3412
3117
  * @param el {HTMLElement}
3413
3118
  * @param attributeName {string} Attribute name. Not case-sensitive.
3414
3119
  * @param defaultValue {*} Default value to use if attribute doesn't exist.
3415
- * @param type {ArgType|function|*[]}
3120
+ * @param type {ArgType|function|Class|*[]}
3416
3121
  * If an array, use the value if it's in the array, otherwise return undefined.
3417
3122
  * If it's a function, pass the value to the function and return the result.
3418
3123
  * @param fallback {*} If the defaultValue is undefiend and type can't be parsed as the given type, use this value.
@@ -3427,8 +3132,11 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
3427
3132
  if (Array.isArray(type))
3428
3133
  return type.includes(val) ? val : fallback;
3429
3134
 
3430
- if (typeof type === 'function')
3431
- return type(val);
3135
+ if (typeof type === 'function') {
3136
+ return type.constructor
3137
+ ? new type(val) // arg type is custom Class
3138
+ : type(val); // arg type is custom function
3139
+ }
3432
3140
 
3433
3141
  // If bool, it's true as long as it exists and its value isn't falsey.
3434
3142
  if (type===ArgType.Bool) {
@@ -3470,6 +3178,24 @@ function getArg(el, attributeName, defaultValue=undefined, type=ArgType.String,
3470
3178
  }
3471
3179
  }
3472
3180
 
3181
+
3182
+ /**
3183
+ * Experimental. Set multiple arguments/attributes all at once.
3184
+ * @param el {HTMLElement}
3185
+ * @param args {Record<string, any>}
3186
+ * @param types {Record<string, ArgType|function|Class>}
3187
+ *
3188
+ * @example
3189
+ * constructor({user, path}={}) {
3190
+ * setArgs(this, arguments[0], {user: User, path: ArgType.String});
3191
+ * }
3192
+ */
3193
+ function setArgs(el, args, types) {
3194
+ for (let name in args)
3195
+ this[name] = getArg(el, name, args[name], types[name] || ArgType.String);
3196
+ }
3197
+
3198
+
3473
3199
  /**
3474
3200
  * @enum */
3475
3201
  var ArgType = {
@@ -3498,6 +3224,158 @@ var ArgType = {
3498
3224
  Eval: 'Eval'
3499
3225
  };
3500
3226
 
3227
+ function defineClass(Class, tagName, extendsTag) {
3228
+ if (!customElements[getName](Class)) { // If not previously defined.
3229
+ tagName = tagName || Util.camelToDashes(Class.name);
3230
+ if (!tagName.includes('-'))
3231
+ tagName += '-element';
3232
+
3233
+ let options = null;
3234
+ if (extendsTag)
3235
+ options = {extends: extendsTag};
3236
+
3237
+ customElements[define](tagName, Class, options);
3238
+ }
3239
+ }
3240
+
3241
+ /**
3242
+ * Create a version of the Solarite class that extends from the given tag name.
3243
+ * Reasons to inherit from this instead of HTMLElement. None of these are all that useful.
3244
+ * 1. customElements.define() is called automatically when you create the first instance.
3245
+ * 2. Calls render() when added to the DOM, if it hasn't been called already.
3246
+ * 3. Child elements are added before constructor is called. But they're also passed to the constructor. (deprecated?)
3247
+ * 4. We can use this.html = r`...` to set html. (deprecated)
3248
+ * 5. We have the onConnect, onFirstConnect, and onDisconnect methods.
3249
+ * Can't figure out how to have these work standalone though, and still be synchronous.
3250
+ * 6. Can we extend from other element types like TR?
3251
+ * 7. Shows default text if render() function isn't defined.
3252
+ *
3253
+ * Advantages to inheriting from HTMLElement
3254
+ * 1. Minimization won't break when it renames the Class and we call customElements.define() on the wrong name.
3255
+ * 2. We can inherit from things like HTMLTableRowElement directly.
3256
+ * 3. There's less magic, since everyone is familiar with defining custom elements.
3257
+ *
3258
+ * @param extendsTag {?string}
3259
+ * @return {Class} */
3260
+ function createSolarite(extendsTag=null) {
3261
+
3262
+ let BaseClass = HTMLElement;
3263
+ if (extendsTag && !extendsTag.includes('-')) {
3264
+ extendsTag = extendsTag.toLowerCase();
3265
+
3266
+ BaseClass = Globals$1.elementClasses[extendsTag];
3267
+ if (!BaseClass) { // TODO: Use Cache
3268
+ BaseClass = document.createElement(extendsTag).constructor;
3269
+ Globals$1.elementClasses[extendsTag] = BaseClass;
3270
+ }
3271
+ }
3272
+
3273
+ /**
3274
+ * Intercept the construct call to auto-define the class before the constructor is called.
3275
+ * @type {HTMLElement} */
3276
+ let HTMLElementAutoDefine = new Proxy(BaseClass, {
3277
+ construct(Parent, args, Class) {
3278
+ defineClass(Class, null, extendsTag);
3279
+
3280
+ // This is a good place to manipulate any args before they're sent to the constructor.
3281
+ // Such as loading them from attributes, if I could find a way to do so.
3282
+
3283
+ // This line is equivalent the to super() call.
3284
+ return Reflect.construct(Parent, args, Class);
3285
+ }
3286
+ });
3287
+
3288
+ return class Solarite extends HTMLElementAutoDefine {
3289
+
3290
+
3291
+ /**
3292
+ * TODO: Make these standalone functions.
3293
+ * Callbacks.
3294
+ * Use onConnect.push(() => ...); to add new callbacks. */
3295
+ onConnect;
3296
+
3297
+ onFirstConnect;
3298
+ onDisconnect;
3299
+
3300
+ /**
3301
+ * @param options {RenderOptions} */
3302
+ constructor(options={}) {
3303
+ super();
3304
+
3305
+ // TODO: Is options.render ever used?
3306
+ if (options.render===true)
3307
+ this.render();
3308
+
3309
+ else if (options.render===false)
3310
+ Globals$1.rendered.add(this); // Don't render on connectedCallback()
3311
+
3312
+ // Add slot children before constructor code executes.
3313
+ // This breaks the styleStaticNested test.
3314
+ // PendingChildren is setup in NodeGroup.instantiateComponent()
3315
+ // TODO: Match named slots.
3316
+ //let ch = Globals.pendingChildren.pop();
3317
+ //if (ch) // TODO: how could there be a slot before render is called?
3318
+ // (this.querySelector('slot') || this).append(...ch);
3319
+
3320
+ /** @deprecated
3321
+ Object.defineProperty(this, 'html', {
3322
+ set(html) {
3323
+ Globals.rendered.add(this);
3324
+ if (typeof html === 'string') {
3325
+ console.warn("Assigning to this.html without the r template prefix.")
3326
+ this.innerHTML = html;
3327
+ }
3328
+ else
3329
+ this.modifications = r(this, html, options);
3330
+ }
3331
+ })*/
3332
+
3333
+ /*
3334
+ let pthis = new Proxy(this, {
3335
+ get(obj, prop) {
3336
+ return Reflect.get(obj, prop)
3337
+ }
3338
+ });
3339
+ this.render = this.render.bind(pthis);
3340
+ */
3341
+ }
3342
+
3343
+ /**
3344
+ * Call render() only if it hasn't already been called. */
3345
+ renderFirstTime() {
3346
+ if (!Globals$1.rendered.has(this) && this.render)
3347
+ this.render();
3348
+ }
3349
+
3350
+ /**
3351
+ * Called automatically by the browser. */
3352
+ connectedCallback() {
3353
+ this.renderFirstTime();
3354
+ if (!Globals$1.connected.has(this)) {
3355
+ Globals$1.connected.add(this);
3356
+ if (this.onFirstConnect)
3357
+ this.onFirstConnect();
3358
+ }
3359
+ if (this.onConnect)
3360
+ this.onConnect();
3361
+ }
3362
+
3363
+ disconnectedCallback() {
3364
+ if (this.onDisconnect)
3365
+ this.onDisconnect();
3366
+ }
3367
+
3368
+
3369
+ static define(tagName=null) {
3370
+ defineClass(this, tagName, extendsTag);
3371
+ }
3372
+ }
3373
+ }
3374
+
3375
+ // Trick to prevent minifier from renaming this method.
3376
+ let define = 'define';
3377
+ let getName = 'getName';
3378
+
3501
3379
  /**
3502
3380
  * Solarite JavasCript UI library.
3503
3381
  * MIT License
@@ -3507,15 +3385,13 @@ var ArgType = {
3507
3385
  /**
3508
3386
  * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
3509
3387
  * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
3510
- let Solarite = new Proxy(createSolarite(), {
3388
+ const Solarite = new Proxy(createSolarite(), {
3511
3389
  apply(self, _, args) {
3512
3390
  return createSolarite(...args)
3513
3391
  }
3514
3392
  });
3515
- let getInputValue = Util.getInputValue;
3516
3393
 
3517
- //Experimental:
3518
3394
  //export {default as watch, renderWatched} from './watch.js'; // unfinished
3519
3395
 
3520
3396
  export default h;
3521
- export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, getInputValue, h, h as r };
3397
+ export { ArgType, Globals$1 as Globals, Solarite, Util as SolariteUtil, Template, delve, getArg, h, h as r, setArgs };