solarite 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/h.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import Template from "./Template.js";
2
- import Util from "./Util.js";
3
2
  import Globals from "./Globals.js";
4
- import {assert} from "./assert.js";
3
+ import toEl from "./toEl.js";
4
+ import Util from "./Util.js";
5
5
 
6
6
  /**
7
7
  * Convert strings to HTMLNodes.
@@ -9,175 +9,130 @@ import {assert} from "./assert.js";
9
9
  * Using h() as a function() will always create a DOM element.
10
10
  *
11
11
  * Features beyond what standard js tagged template strings do:
12
- * 1. r`` sub-expressions
12
+ * 1. h`` sub-expressions
13
13
  * 2. functions, nodes, and arrays of nodes as sub-expressions.
14
- * 3. html-escape all expressions by default, unless wrapped in r()
14
+ * 3. html-escape all expressions by default, unless wrapped in h()
15
15
  * 4. event binding
16
16
  * 5. TODO: list more
17
17
  *
18
+ * General rule:
19
+ * If h() is a function with null or an HTMLElement as its first argument create a Node.
20
+ * Otherwise create a template
21
+ *
18
22
  * Currently supported:
19
- * 1. h(el, options)`<b>${'Hi'}</b>` // Create template and render its nodes to el.
20
- * 2. h(el, template, ?options) // Render the Template created by #1 to element.
21
23
  *
22
- * 3. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
24
+ * Create Tempataes
25
+ * 1. h`<b>Hello</b> ${'World'}!` // Create Template that can later be used to create nodes.
26
+ * 2. h('<b>Hello</b><u>Goodbye</u>'); // Create Template from string, that can later be used to create nodes.
23
27
  *
24
- * 4. h('Hello'); // Create single text node.
25
- * 5. h('<b>Hello</b>'); // Create single HTMLElement
26
- * 6. h('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
27
- * 7. h()`Hello<b>${'World'}!</b>` // Same as 4-6, but evaluates the string as a Solarite template, which
28
- * // includes properly handling nested components and r`` sub-expressions.
29
- * 8. h(template) // Render Template created by #1.
28
+ * Add children to an element.
29
+ * 3. h(el, h`<b>${'Hi'}</b>`, ?options)
30
+ * 4. h(el, ?options)`<b>${'Hi'}</b>` // typical path used in render(). Create template and render its nodes to el.
30
31
  *
31
- * 9. h({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
32
- * 10. h(string, object, ...) // JSX TODO
32
+ * Create top-level element
33
+ * 5. h()`Hello<b>${'World'}!</b>`
34
+ *
35
+ * 6. h(string, object, ...) // Used for JSX
33
36
  * @param htmlStrings {?HTMLElement|string|string[]|function():Template|{render:function()}}
34
37
  * @param exprs {*[]|string|Template|Object}
35
- * @return {Node|HTMLElement|Template} */
38
+ * @return {Node|HTMLElement|Template|Function} */
36
39
  export default function h(htmlStrings=undefined, ...exprs) {
37
40
 
38
- if (htmlStrings === undefined && !exprs.length && arguments.length)
39
- throw new Error('h() cannot be called with undefined.');
40
-
41
- // TODO: Make this a more flat if/else and call other functions for the logic.
42
- if (htmlStrings instanceof Node) {
43
- let parent = htmlStrings, template = exprs[0];
41
+ // 1. Tagged template: h`<div>...</div>`
42
+ if (Array.isArray(arguments[0])) {
43
+ return new Template(arguments[0], exprs);
44
+ }
44
45
 
45
- // 1
46
- if (!(exprs[0] instanceof Template)) {
47
- if (parent.shadowRoot)
48
- parent.innerHTML = ''; // Remove shadowroot. TODO: This could mess up paths?
46
+ // 2. String to template, or JSX factory form h(tag, props, ...children)
47
+ else if (typeof arguments[0] === 'string' || arguments[0] instanceof String) {
48
+ let tagOrHtml = arguments[0];
49
49
 
50
- let options = exprs[0];
50
+ // 2a. JSX: h("tag", {props}, ...children)
51
+ if (exprs.length && (typeof exprs[0] === 'object' || exprs[0] === null)) {
52
+ let tag = tagOrHtml + '';
53
+ let props = exprs[0] || {};
54
+ let children = exprs.slice(1);
51
55
 
52
- // Return a tagged template function that applies the tagged themplate to parent.
53
- let taggedTemplate = (htmlStrings, ...exprs) => {
54
- Globals.rendered.add(parent)
55
- let template = new Template(htmlStrings, exprs);
56
- return template.render(parent, options);
57
- }
58
- return taggedTemplate;
56
+ return Template.fromJsx(tag, props, children);
59
57
  }
60
58
 
61
- // 2. Render template created by #4 to element.
62
- else { // instanceof Template
63
- let options = exprs[1];
64
- template.render(parent, options);
65
-
66
- // Append on the first go.
67
- if (!parent.childNodes.length && this) {
68
- // TODO: Is this ever executed?
69
- debugger;
70
- parent.append(this.rootNg.getParentNode());
71
- }
59
+ // 2b. Plain html string => template: h('<div>...</div>')
60
+ else {
61
+ let html = tagOrHtml;
62
+ // If it starts with whitespace and then a tag, trim it.
63
+ if (html.match(/^\s^</))
64
+ html = html.trim();
65
+ return new Template([html], []);
72
66
  }
73
67
  }
74
68
 
75
- // 3. Path if used as a template tag.
76
- else if (Array.isArray(htmlStrings)) {
77
- return new Template(htmlStrings, exprs);
78
- }
79
-
80
- else if (typeof htmlStrings === 'string' || htmlStrings instanceof String) {
81
- // 10. JSX
82
- if (typeof exprs[0] === 'object') {
83
- let tag = htmlStrings;
84
- let props = exprs[0] || {};
85
- let children = exprs.slice(1);
86
-
87
- let templateHtmlStrings = [];
88
- let templateExprs = [];
69
+ else if (arguments[0] instanceof HTMLElement || arguments[0] instanceof DocumentFragment) {
89
70
 
90
- // TODO How to know which children are static html and which are expression placeholders?
91
- // Perhaps we have to treat every text child as a string?
71
+ // 3. Render template to element: h(el, template)
72
+ if (arguments[1] instanceof Template) {
92
73
 
93
- assert(templateHtmlStrings.length === templateExprs.length+1);
94
- return new Template(templateHtmlStrings, templateExprs);
74
+ /** @type Template */
75
+ let template = arguments[1];
76
+ let parent = arguments[0];
77
+ let options = arguments[2];
78
+ template.render(parent, options);
95
79
  }
96
80
 
81
+ // 4. Render tagged template to element: h(el)`<div>...</div>`
82
+ else {
83
+ let parent = arguments[0], options = arguments[1];
97
84
 
98
- // If it starts with a string, trim both ends.
99
- // TODO: Also trim if it ends with whitespace?
100
- if (htmlStrings.match(/^\s^</))
101
- htmlStrings = htmlStrings.trim();
102
-
103
- // We create a new one each time because otherwise
104
- // the returned fragment will have its content replaced by a subsequent call.
105
- let templateEl = document.createElement('template');
106
- templateEl.innerHTML = htmlStrings;
107
-
108
- // 4+5. Return Node if there's one child.
109
- let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
110
- if (relevantNodes.length === 1)
111
- return relevantNodes[0];
85
+ // Remove shadowroot if present. TODO: This could mess up paths?
86
+ if (parent.shadowRoot)
87
+ parent.innerHTML = '';
112
88
 
113
- // 6. Otherwise return DocumentFragment.
114
- return templateEl.content;
89
+ // Return a tagged template function that applies the tagged template to parent.
90
+ let renderTemplate = (htmlStrings, ...exprs) => {
91
+ Globals.rendered.add(parent)
92
+ let template = new Template(htmlStrings, exprs);
93
+ return template.render(parent, options);
94
+ }
95
+ return renderTemplate;
96
+ }
115
97
  }
116
98
 
117
- // 7. Create a static element
118
- else if (htmlStrings === undefined) {
99
+ // 5. Create a static element: h()`<div></div>`
100
+ else if (!arguments.length) {
119
101
  return (htmlStrings, ...exprs) => {
120
- //Globals.rendered.add(parent)
121
102
  let template = h(htmlStrings, ...exprs);
122
- return template.render();
103
+ return toEl(template);
123
104
  }
124
105
  }
125
106
 
126
- // 8.
127
- else if (htmlStrings instanceof Template) {
128
- return htmlStrings.render();
129
- }
130
-
131
-
132
- // 9. Create dynamic element with render() function.
107
+ // 6. Help toEl() with objects: h(this)`<div>...</div>` inside an object's render()
108
+ // Intercepts the main h(this)`...` function call inside render().
133
109
  // TODO: This path doesn't handle embeds like data-id="..."
134
- else if (typeof htmlStrings === 'object') {
135
- let obj = htmlStrings;
110
+ else if (typeof arguments[0] === 'object' && Globals.objToEl.has(arguments[0])) {
111
+ let obj = arguments[0];
136
112
 
137
- if (obj.constructor.name !== 'Object')
113
+ if (obj.constructor.name !== 'Object')
138
114
  throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
139
115
 
116
+ // Jsx with h(this, <jsx>)
117
+ if (arguments[1] instanceof Template) {
118
+ let template = arguments[1];
119
+ let el = template.render();
120
+ Globals.objToEl.set(obj, el);
121
+ }
140
122
 
141
- // Special rebound render path, called by normal path.
142
- // Intercepts the main r`...` function call inside render().
143
- if (Globals.objToEl.has(obj)) {
123
+ // h(this)`<div>...</div>`
124
+ else
144
125
  return function(...args) {
145
- let template = h(...args);
146
- let el = template.render();
126
+ let template = h(...args);
127
+ let el = template.render();
147
128
  Globals.objToEl.set(obj, el);
148
129
  }.bind(obj);
149
- }
150
-
151
- // Normal path
152
- else {
153
- Globals.objToEl.set(obj, null);
154
- obj[renderF](); // Calls the Special rebound render path above, when the render function calls r(this)
155
- let el = Globals.objToEl.get(obj);
156
- Globals.objToEl.delete(obj);
157
-
158
- for (let name in obj)
159
- if (typeof obj[name] === 'function')
160
- el[name] = obj[name].bind(el); // Make the "this" of functions be el.
161
- // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
162
- // <my-element arg=${{myFunc() { return this }}}
163
- else
164
- el[name] = obj[name];
165
-
166
- // Bind id's
167
- // This doesn't work for id's referenced by attributes.
168
- // for (let idEl of el.querySelectorAll('[id],[data-id]')) {
169
- // Util.bindId(el, idEl);
170
- // Util.bindId(obj, idEl);
171
- // }
172
- // TODO: Bind styles
173
-
174
- return el;
175
- }
176
130
  }
131
+ // TODO: Handle other primitive types?
132
+ else if (Util.isFalsy(arguments[0]))
133
+ return new Template();
177
134
 
178
135
  else
179
- throw new Error('Unsupported arguments.')
136
+ throw new Error('h() does not support argument of type: ' + (arguments[0] ? typeof arguments[0] : arguments[0]))
180
137
  }
181
138
 
182
- // Trick to prevent minifier from renaming this function.
183
- let renderF = 'render';
package/src/hash.js CHANGED
@@ -9,8 +9,8 @@ export function getObjectId(obj) {
9
9
  // return obj.toString(); // This fails to detect when a function's bound variables changes.
10
10
 
11
11
  let result = objectIds.get(obj);
12
- if (!result) { // convert to string, store in result, then add 1 to lastObjectId.
13
- result = '~\f' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
12
+ if (result===undefined) { // convert to string, store in result, then add 1 to lastObjectId.
13
+ result = '~@' + (lastObjectId++); // We use a unique, 2-byte prefix to ensure it doesn't collide w/ strings not from getObjectId()
14
14
  objectIds.set(obj, result)
15
15
  }
16
16
  return result;
@@ -20,7 +20,8 @@ export function getObjectId(obj) {
20
20
  * Control how JSON.stringify() handles Nodes and Functions.
21
21
  * Normally, we'd pass a replacer() function argument to JSON.stringify() to handle Nodes and Functions.
22
22
  * But that makes JSON.stringify() take twice as long to run.
23
- * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty. */
23
+ * Adding a toJSON method globally on these object prototypes doesn't incur that performance penalty.
24
+ * TODO: This needs to be benchmarked again after the json rewrite in Chrome 138. */
24
25
  let isHashing = true;
25
26
  function toJSON() {
26
27
  return isHashing ? getObjectId(this) : this
@@ -48,26 +49,27 @@ export function getObjectHash(obj) {
48
49
  // Sometimes these get unassigned by Chrome and Brave 119, as well as Firefox, seemingly randomly!
49
50
  // The same tests sometimes pass, sometimes fail, even after browser and OS restarts.
50
51
  // So we check the assignments on every run of getObjectHash()
52
+ // TODO: Cache references to Node.prototype and Function.prototype:
51
53
  if (Node.prototype.toJSON !== toJSON) {
52
54
  Node.prototype.toJSON = toJSON;
53
55
  if (Function.prototype.toJSON !== toJSON) // Will it only unmap one but not the other?
54
56
  Function.prototype.toJSON = toJSON;
55
57
  }
56
58
 
57
- let result;
58
59
  isHashing = true;
59
60
  try {
60
- result = JSON.stringify(obj);
61
+ return JSON.stringify(obj);
61
62
  }
62
63
  catch(e) {
63
- result = getObjectHashCircular(obj);
64
+ return getObjectHashCircular(obj);
65
+ }
66
+ finally {
67
+ isHashing = false;
64
68
  }
65
- isHashing = false;
66
- return result;
67
69
  }
68
70
 
69
71
  /**
70
- * Slower hashing method that supports.
72
+ * Slower hashing method that supports circular references.
71
73
  * @param obj
72
74
  * @returns {string} */
73
75
  function getObjectHashCircular(obj) {
package/src/toEl.js ADDED
@@ -0,0 +1,83 @@
1
+ import Globals from "./Globals.js";
2
+ import Template from "./Template.js";
3
+ import Util from "./Util.js";
4
+
5
+ /**
6
+ * Convert a template, string, or object into a DOM Node or Element
7
+ *
8
+ * 1. toEl('Hello'); // Create single text node.
9
+ * 2. toEl('<b>Hello</b>'); // Create single HTMLElement
10
+ * 3. toEl('<b>Hello</b><u>Goodbye</u>'); // Create document fragment because there's more than one node.
11
+ * 4. toEl(template) // Render Template created by h`<html>` or h();
12
+ * 5. toEl({render(){...}}) // Pass an object with a render method, and optionally other props/methods.
13
+ * @param arg {string|Template|{render:()=>void}}
14
+ * @returns {Node|DocumentFragment|HTMLElement} */
15
+ export default function toEl(arg) {
16
+
17
+ if (typeof arg === 'string') {
18
+ let html = arg;
19
+
20
+ // If it's an element with whitespace before or after it, trim both ends.
21
+ if (html.match(/^\s^<\S+/) || html.match(/\S+>\s+$/))
22
+ html = html.trim();
23
+
24
+ // We create a new one each time because otherwise
25
+ // the returned fragment will have its content replaced by a subsequent call.
26
+ let templateEl = Globals.doc.createElement('template');
27
+ templateEl.innerHTML = html;
28
+
29
+ // 1+2. Return Node if there's one child.
30
+ let relevantNodes = Util.trimEmptyNodes(templateEl.content.childNodes);
31
+ if (relevantNodes.length === 1)
32
+ return relevantNodes[0];
33
+
34
+ // 3. Otherwise return DocumentFragment.
35
+ return templateEl.content;
36
+ }
37
+
38
+ // 4.
39
+ if (arg instanceof Template) {
40
+ return arg.render();
41
+ }
42
+
43
+ // 5. Create dynamic element from an object with a render() function.
44
+ // TODO: This path doesn't handle embeds like data-id="..."
45
+ else if (arg && typeof arg === 'object') {
46
+ let obj = arg;
47
+
48
+ if (obj.constructor.name !== 'Object')
49
+ throw new Error(`Solarate Web Component class ${obj.constructor?.name} must extend HTMLElement.`);
50
+
51
+ // Normal path
52
+ if (!Globals.objToEl.has(obj)) {
53
+ Globals.objToEl.set(obj, null);
54
+ obj[renderF](); // Calls the Special rebound render path above, when the render function calls h(this)
55
+ let el = Globals.objToEl.get(obj);
56
+ Globals.objToEl.delete(obj);
57
+
58
+ for (let name in obj)
59
+ if (typeof obj[name] === 'function')
60
+ el[name] = obj[name].bind(el); // Make the "this" of functions be el.
61
+ // TODO: But this doesn't work for passing an object with functions as a constructor arg via an attribute:
62
+ // <my-element arg=${{myFunc() { return this }}}
63
+ else
64
+ el[name] = obj[name];
65
+
66
+ // Bind id's
67
+ // This doesn't work for id's referenced by attributes.
68
+ // for (let idEl of el.querySelectorAll('[id],[data-id]')) {
69
+ // Util.bindId(el, idEl);
70
+ // Util.bindId(obj, idEl);
71
+ // }
72
+ // TODO: Bind styles
73
+
74
+ return el;
75
+ }
76
+ }
77
+
78
+ throw new Error('toEl() does not support argument of type: ' + (arg ? typeof arg : arg));
79
+ }
80
+
81
+
82
+ // Trick to prevent minifier from renaming this function.
83
+ let renderF = 'render';
package/src/udomdiff.js CHANGED
@@ -16,8 +16,6 @@
16
16
  * PERFORMANCE OF THIS SOFTWARE.
17
17
  */
18
18
 
19
- import NodeGroup from "./NodeGroup.js";
20
-
21
19
  /**
22
20
  * @param {Node} parentNode The container where children live
23
21
  * @param {Node[]} a The list of current/live children
@@ -28,11 +26,6 @@ import NodeGroup from "./NodeGroup.js";
28
26
  * @returns {Node[]} The same list of future children.
29
27
  */
30
28
  const udomdiff = (parentNode, a, b, before) => {
31
- //#IFDEV
32
- // if (parentNode instanceof ExprPath)
33
- // parentNode.verify();
34
- //#ENDIF
35
-
36
29
  const bLength = b.length;
37
30
  let aEnd = a.length;
38
31
  let bEnd = bLength;
@@ -54,13 +47,6 @@ const udomdiff = (parentNode, a, b, before) => {
54
47
  while (bStart < bEnd) {
55
48
  let bNode = b[bStart++];
56
49
  parentNode.insertBefore(bNode, node);
57
-
58
- //#IFDEV
59
- if (bNode instanceof NodeGroup)
60
- bNode.verify();
61
- // if (parentNode instanceof ExprPath)
62
- // parentNode.verify();
63
- //#ENDIF
64
50
  }
65
51
  }
66
52
  // remove head or tail: fast path
@@ -70,13 +56,6 @@ const udomdiff = (parentNode, a, b, before) => {
70
56
  let aNode = a[aStart];
71
57
  if (!map || !map.has(aNode)) {
72
58
  parentNode.removeChild(aNode);
73
-
74
- //#IFDEV
75
- if (aNode instanceof NodeGroup)
76
- aNode.verify();
77
- // if (parentNode instanceof ExprPath)
78
- // parentNode.verify();
79
- //#ENDIF
80
59
  }
81
60
  aStart++;
82
61
  }
@@ -113,24 +92,10 @@ const udomdiff = (parentNode, a, b, before) => {
113
92
  a2,
114
93
  b2.nextSibling
115
94
  );
116
- //#IFDEV
117
- if (a2 instanceof NodeGroup)
118
- a2.verify();
119
- // if (parentNode instanceof ExprPath)
120
- // parentNode.verify();
121
- //#ENDIF
122
95
 
123
96
  let bNode = b[--bEnd];
124
97
  parentNode.insertBefore(bNode, node);
125
98
 
126
- //#IFDEV
127
- if (bNode instanceof NodeGroup)
128
- bNode.verify();
129
- // if (parentNode instanceof ExprPath)
130
- // parentNode.verify();
131
-
132
- //#ENDIF
133
-
134
99
  // mark the future index as identical (yeah, it's dirty, but cheap 👍)
135
100
  // The main reason to do this, is that when a[aEnd] will be reached,
136
101
  // the loop will likely be on the fast path, as identical to b[bEnd].
@@ -178,14 +143,6 @@ const udomdiff = (parentNode, a, b, before) => {
178
143
  while (bStart < index) {
179
144
  let bNode = b[bStart++];
180
145
  parentNode.insertBefore(bNode, node);
181
-
182
- //#IFDEV
183
- if (bNode instanceof NodeGroup)
184
- bNode.verify();
185
- // if (parentNode instanceof ExprPath)
186
- // parentNode.verify();
187
-
188
- //#ENDIF
189
146
  }
190
147
  }
191
148
  // if the effort wasn't good enough, fallback to a replace,
@@ -198,13 +155,6 @@ const udomdiff = (parentNode, a, b, before) => {
198
155
  bNode,
199
156
  aNode
200
157
  );
201
-
202
- //#IFDEV
203
- if (aNode instanceof NodeGroup)
204
- aNode.verify();
205
- // if (parentNode instanceof ExprPath)
206
- // parentNode.verify();
207
- //#ENDIF
208
158
  }
209
159
  }
210
160
  // otherwise move the source forward, 'cause there's nothing to do
@@ -217,13 +167,6 @@ const udomdiff = (parentNode, a, b, before) => {
217
167
  else {
218
168
  let aNode = a[aStart++];
219
169
  parentNode.removeChild(aNode);
220
-
221
- //#IFDEV
222
- if (aNode instanceof NodeGroup)
223
- aNode.verify();
224
- // if (parentNode instanceof ExprPath)
225
- // parentNode.verify();
226
- //#ENDIF
227
170
  }
228
171
  }
229
172
  }