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,7 +1,8 @@
1
- import {assert} from "../util/Errors.js";
2
- import ExprPath, {PathType, getNodePath} from "./ExprPath.js";
3
- import {div, htmlContext, isEvent} from "./Util.js";
1
+ import {assert} from "./assert.js";
2
+ import ExprPath, {ExprPathType, getNodePath} from "./ExprPath.js";
3
+ import Util from "./Util.js";
4
4
  import Globals from "./Globals.js";
5
+ import HtmlParser from "./HtmlParser.js";
5
6
 
6
7
  /**
7
8
  * A Shell is created from a tagged template expression instantiated as Nodes,
@@ -9,105 +10,75 @@ import Globals from "./Globals.js";
9
10
  * Only one Shell is created for all the items in a loop.
10
11
  *
11
12
  * When a NodeGroup is created from a Template's html strings,
12
- * the NodeGroup then clones the Shell's fragmentn to be its nodes. */
13
+ * the NodeGroup then clones the Shell's fragment to be its nodes. */
13
14
  export default class Shell {
14
15
 
15
16
  /**
16
- * @type {DocumentFragment} DOM parent of the shell nodes. */
17
+ * @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
17
18
  fragment;
18
19
 
19
20
  /** @type {ExprPath[]} Paths to where expressions should go. */
20
21
  paths = [];
21
22
 
22
- // Embeds and ids
23
- events = [];
23
+ // Elements with events. Not yet used.
24
+ // events = [];
24
25
 
25
26
  /** @type {int[][]} Array of paths */
26
27
  ids = [];
28
+
29
+ /** @type {int[][]} Array of paths */
27
30
  scripts = [];
31
+
32
+ /** @type {int[][]} Array of paths */
28
33
  styles = [];
29
34
 
35
+ /** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
30
36
  staticComponents = [];
31
37
 
38
+ /** @type {{path:int[], attribs:Record<string, string>}[]} */
39
+ //componentAttribs = [];
40
+
32
41
 
33
42
 
34
43
  /**
35
44
  * Create the nodes but without filling in the expressions.
36
45
  * This is useful because the expression-less nodes created by a template can be cached.
37
- * @param html {string[]} */
46
+ * @param html {string[]} Html strings, split on places where an expression exists. */
38
47
  constructor(html=null) {
39
48
  if (!html)
40
49
  return;
41
50
 
42
51
  //#IFDEV
43
- this.html = html.join('');
52
+ this._html = html.join('');
44
53
  //#ENDIF
45
54
 
46
- // 1. Add placeholders
47
- // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
48
- let placeholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
49
-
50
- let buffer = [];
51
- let commentPlaceholder = `<!--!✨!-->`;
52
- let componentNames = {};
53
-
54
- htmlContext(null); // Reset the context.
55
- for (let i=0; i<html.length; i++) {
56
- let lastHtml = html[i];
57
- let context = htmlContext(lastHtml);
58
-
59
- // Swap out Embedded Solarite Components with ${} attributes.
60
- // Later, NodeGroup.render() will search for these and replace them with the real components.
61
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
62
- if (context === htmlContext.Attribute) {
63
-
64
- let lastIndex, lastMatch;
65
- lastHtml.replace(/<[a-z][a-z0-9]*-[a-z0-9-]+/ig, (match, index) => {
66
- lastIndex = index+1; // +1 for after opening <
67
- lastMatch = match.slice(1);
68
- })
69
-
70
- if (lastMatch) {
71
- let newTagName = lastMatch + '-solarite-placeholder';
72
- lastHtml = lastHtml.slice(0, lastIndex) + newTagName + lastHtml.slice(lastIndex + lastMatch.length);
73
- componentNames[lastMatch] = newTagName
74
- }
75
- }
76
-
77
- buffer.push(lastHtml);
78
- //console.log(lastHtml, context)
79
- if (i < html.length-1)
80
- if (context === htmlContext.Text)
81
- buffer.push(commentPlaceholder) // Comment Placeholder. because we can't put text in between <tr> tags for example.
82
- else
83
- buffer.push(String.fromCharCode(placeholder+i));
55
+ if (html.length === 1 && !html[0].match(/[<&]/)) {
56
+ this.fragment = document.createTextNode(html[0]);
57
+ return;
84
58
  }
85
59
 
86
- // 2. Create elements from html with placeholders.
60
+
61
+ // 1. Add placeholders
62
+ let joinedHtml = Shell.addPlaceholders(html);
63
+
87
64
  let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
88
- let joinedHtml = buffer.join('');
89
-
90
- // Replace '-solarite-placeholder' close tags.
91
- // TODO: is there a better way? What if the close tag is inside a comment?
92
- for (let name in componentNames)
93
- joinedHtml = joinedHtml.replaceAll(`</${name}>`, `</${componentNames[name]}>`);
94
-
95
- if (joinedHtml)
96
- template.innerHTML = joinedHtml;
97
- else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
98
- template.content.append(document.createTextNode(''))
65
+ if (joinedHtml)
66
+ template.innerHTML = joinedHtml;
67
+ else // Create one text node, so shell isn't empty and NodeGroups created from it have something to point the startNode and endNode at.
68
+ template.content.append(document.createTextNode(''))
99
69
  this.fragment = template.content;
100
70
 
101
- // 3. Find placeholders
71
+ // 2. Find placeholders
102
72
  let node;
103
73
  let toRemove = [];
104
- const walker = document.createTreeWalker(template.content, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
74
+ let placeholdersUsed = 0;
75
+ const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
105
76
  while (node = walker.nextNode()) {
106
77
 
107
78
  // Remove previous after each iteration, so paths will still be calculated correctly.
108
79
  toRemove.map(el => el.remove());
109
80
  toRemove = [];
110
-
81
+
111
82
  // Replace attributes
112
83
  if (node.nodeType === 1) {
113
84
  for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
@@ -115,7 +86,8 @@ export default class Shell {
115
86
  // Whole attribute
116
87
  let matches = attr.name.match(/^[\ue000-\uf8ff]$/)
117
88
  if (matches) {
118
- this.paths.push(new ExprPath(null, node, PathType.Multiple));
89
+ this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
90
+ placeholdersUsed ++;
119
91
  node.removeAttribute(matches[0]);
120
92
  }
121
93
 
@@ -124,16 +96,17 @@ export default class Shell {
124
96
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
125
97
  if (parts.length > 1) {
126
98
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
127
- let type = isEvent(attr.name) ? PathType.Event : PathType.Value;
99
+ let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
128
100
 
129
101
  this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
102
+ placeholdersUsed += parts.length - 1;
130
103
  node.setAttribute(attr.name, parts.join(''));
131
104
  }
132
105
  }
133
106
  }
134
107
  }
135
108
  // Replace comment placeholders
136
- else if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === '!✨!') {
109
+ else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
137
110
 
138
111
  // Get or create nodeBefore.
139
112
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
@@ -158,12 +131,14 @@ export default class Shell {
158
131
  }
159
132
  /*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
160
133
 
161
-
162
-
163
- let path = new ExprPath(nodeBefore, nodeMarker, PathType.Content);
164
-
134
+ let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
165
135
  this.paths.push(path);
136
+ placeholdersUsed ++;
166
137
  }
138
+
139
+ else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
140
+ throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
141
+
167
142
 
168
143
 
169
144
  // Sometimes users will comment out a block of html code that has expressions.
@@ -174,8 +149,9 @@ export default class Shell {
174
149
  let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
175
150
  for (let i=0; i<parts.length-1; i++) {
176
151
  let path = new ExprPath(node.previousSibling, node)
177
- path.type = PathType.Comment;
152
+ path.type = ExprPathType.Comment;
178
153
  this.paths.push(path);
154
+ placeholdersUsed ++;
179
155
  }
180
156
  }
181
157
 
@@ -193,8 +169,9 @@ export default class Shell {
193
169
  }
194
170
 
195
171
  for (let i=0, node; node=placeholders[i]; i++) {
196
- let path = new ExprPath(node.previousSibling, node, PathType.Content);
172
+ let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
197
173
  this.paths.push(path);
174
+ placeholdersUsed ++;
198
175
 
199
176
  /*#IFDEV*/path.verify();/*#ENDIF*/
200
177
  }
@@ -206,17 +183,17 @@ export default class Shell {
206
183
  }
207
184
  toRemove.map(el => el.remove());
208
185
 
186
+ // Less than or equal because there can be one path to multiple expressions
187
+ // if those expressions are in the same attribute value.
188
+ if (placeholdersUsed !== html.length-1)
189
+ throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
190
+
209
191
  // Handle solarite-placeholder's.
210
- // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
211
- //if (componentNames.size)
212
- // this.components = [...this.fragment.querySelectorAll([...componentNames].join(','))]
213
192
 
214
- // Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
193
+ // 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
215
194
  // that happens in NodeGroup.applyComponentExprs()
216
- for (let el of this.fragment.querySelectorAll('[is]')) {
217
- el.setAttribute('_is', el.getAttribute('is'))
218
- // this.components.push(el);
219
- }
195
+ for (let el of this.fragment.querySelectorAll('[is]'))
196
+ el.setAttribute('_is', el.getAttribute('is'));
220
197
 
221
198
  for (let path of this.paths) {
222
199
  if (path.nodeBefore)
@@ -224,16 +201,64 @@ export default class Shell {
224
201
  path.nodeMarkerPath = getNodePath(path.nodeMarker)
225
202
 
226
203
  // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
227
- if (path.type === PathType.Value && path.nodeMarker.nodeType === 1 && /*path.nodeMarker !== template.content.children[0] &&*/
204
+ if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
228
205
  (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
229
- path.type = PathType.Component;
206
+ path.type = ExprPathType.ComponentAttribValue;
230
207
  }
231
208
  }
232
209
 
233
210
  this.findEmbeds();
234
211
 
235
212
  /*#IFDEV*/this.verify();/*#ENDIF*/
236
- } // end constructor
213
+ }
214
+
215
+ /**
216
+ * 1. Add a Unicode placeholder char for where expressions go within attributes.
217
+ * 2. Add a comment placeholder for where expressions are children of other nodes.
218
+ * 3. Append -solarite-placeholder to the tag names of custom components so that we can wait to instantiate them later.
219
+ * @param htmlChunks {string[]}
220
+ * @returns {string} */
221
+ static addPlaceholders(htmlChunks) {
222
+ let tokens = [];
223
+
224
+ function addToken(token, context) {
225
+
226
+ if (context === HtmlParser.Tag) {
227
+ // Find Solarite Components tags and append -solarite-placeholder to their tag names
228
+ // and give them a solarite-placeholder attribute so we can easily find them later.
229
+ // This way we can gather their constructor arguments and their children before we call their constructor.
230
+ // Later, NodeGroup.instantiateComponent() will replace them with the real components.
231
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
232
+ token = token.replace(/^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i, match => match + '-solarite-placeholder solarite-placeholder');
233
+ }
234
+ tokens.push(token)
235
+ }
236
+
237
+ let htmlParser = new HtmlParser(); // Reset the context.
238
+ for (let i = 0; i < htmlChunks.length; i++) {
239
+ let lastHtml = htmlChunks[i];
240
+
241
+ // Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
242
+ let lastIndex = 0;
243
+ let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
244
+ if (lastIndex !== index) {
245
+ let token = html.slice(lastIndex, index);
246
+ addToken(token, oldContext);
247
+ }
248
+ lastIndex = index;
249
+ });
250
+
251
+ // Insert placeholders
252
+ if (i < htmlChunks.length - 1) {
253
+ if (context === HtmlParser.Text)
254
+ tokens.push(commentPlaceholder) // Comment Placeholder. because we can't put text in between <tr> tags for example.
255
+ else
256
+ tokens.push(String.fromCharCode(attribPlaceholder + i));
257
+ }
258
+ }
259
+
260
+ return tokens.join('');
261
+ }
237
262
 
238
263
  /**
239
264
  * We find the path to every embed here once in the Shell, instead of every time a NodeGroup is instantiated.
@@ -245,36 +270,30 @@ export default class Shell {
245
270
  * this.staticComponents */
246
271
  findEmbeds() {
247
272
  this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el))
273
+
274
+ // TODO: only find styles that have ExprPaths in them?
248
275
  this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el))
249
276
 
250
277
  let idEls = this.fragment.querySelectorAll('[id],[data-id]');
251
-
252
278
 
253
279
  // Check for valid id names.
254
280
  for (let el of idEls) {
255
281
  let id = el.getAttribute('data-id') || el.getAttribute('id')
256
- if (div.hasOwnProperty(id))
282
+ if (Globals.div.hasOwnProperty(id))
257
283
  throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
258
284
  }
259
285
 
260
-
261
286
  this.ids = Array.prototype.map.call(idEls, el => getNodePath(el))
262
287
 
263
- // Events (not yet used)
264
288
  for (let el of this.fragment.querySelectorAll('*')) {
265
- for (let attrib of el.attributes)
266
- if (isEvent(attrib.name))
267
- this.events.push([attrib.name, getNodePath(el)])
268
-
269
289
  if (el.tagName.includes('-') || el.hasAttribute('_is'))
270
290
 
271
- // Dynamic components have attributes with expression values.
291
+ // Dynamic components are components that have attributes with expression values.
272
292
  // They are created from applyExprs()
273
293
  // But static components are created in a separate path inside the NodeGroup constructor.
274
294
  if (!this.paths.find(path => path.nodeMarker === el))
275
295
  this.staticComponents.push(getNodePath(el));
276
296
  }
277
-
278
297
  }
279
298
 
280
299
  /**
@@ -303,3 +322,11 @@ export default class Shell {
303
322
  //#ENDIF
304
323
  }
305
324
 
325
+
326
+ const commentPlaceholder = `<!--!✨!-->`;
327
+
328
+
329
+ // We increment the placeholder char as we go because nodes can't have the same attribute more than once.
330
+ const attribPlaceholder = 0xe000; // https://en.wikipedia.org/wiki/Private_Use_Areas 6400.
331
+
332
+
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Solarite JavasCript UI library.
3
+ * MIT License
4
+ * https://vorticode.github.io/solarite/
5
+ */
6
+
7
+ export default function h(htmlStrings?: HTMLElement | string | string[] | Function | {render: Function}, ...exprs: any[]): Node | HTMLElement | Template | Function;
8
+ export { default as h, default as r } from './h.js';
9
+
10
+ export const ArgType: {
11
+ Bool: string;
12
+ Int: string;
13
+ Float: string;
14
+ String: string;
15
+ Json: string;
16
+ Eval: string;
17
+ }
18
+
19
+ export function getArg(el:HTMLElement, attributeName:string, defaultValue?:any,
20
+ type?:typeof ArgType[keyof typeof ArgType] | Function | any[], fallback?:any): any;
21
+
22
+ export interface RenderOptions {
23
+ styles?: boolean;
24
+ scripts?: boolean;
25
+ ids?: boolean;
26
+ render?: boolean;
27
+ }
28
+
29
+ export class Template {
30
+ exprs: (Template|string|Function)[];
31
+ html: string[];
32
+ constructor(htmlStrings: string[], exprs: any[]);
33
+ render(el?: HTMLElement, options?: RenderOptions): DocumentFragment | HTMLElement | null;
34
+ getExactKey(): string;
35
+ getCloseKey(): string;
36
+ }
37
+
38
+ export function delve(obj: object, path: string[], createVal?: any): any;
39
+
40
+
41
+
42
+ // Experimental:
43
+ //--------------
44
+
45
+ // Globals object
46
+ export const Globals: {
47
+ componentArgsHash: WeakMap<any, any>;
48
+ connected: WeakSet<HTMLElement>;
49
+ currentExprPath: any;
50
+ div: HTMLDivElement;
51
+ elementClasses: {[key: string]: typeof Node};
52
+ htmlProps: {[key: string]: boolean};
53
+ nodeEvents: WeakMap<Node, {[eventName: string]: [Function, Function, any[]]}>;
54
+ nodeGroups: WeakMap<HTMLElement, any>;
55
+ objToEl: WeakMap<any, any>;
56
+ rendered: WeakSet<HTMLElement>;
57
+ rendering: WeakSet<HTMLElement>;
58
+ shells: WeakMap<string[], any>;
59
+ reset: Function;
60
+ count: number;
61
+ };
62
+
@@ -4,31 +4,33 @@
4
4
  * https://vorticode.github.io/solarite/
5
5
  */
6
6
 
7
+ import h from './h.js';
8
+ export default h;
9
+ export {default as h, default as r} from './h.js'; //Named exports for h() are deprecated.
10
+ export {default as delve} from './delve.js';
11
+ export {getArg, ArgType} from './getArg.js';
12
+ export {default as Template} from './Template.js';
13
+
14
+
15
+
16
+ // Experimental:
17
+ //--------------
18
+ export {setArgs} from './getArg.js';
7
19
 
8
20
  import createSolarite from "./createSolarite.js";
9
21
 
10
22
  /**
11
23
  * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
12
24
  * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
13
- let Solarite = new Proxy(createSolarite(), {
25
+ const Solarite = new Proxy(createSolarite(), {
14
26
  apply(self, _, args) {
15
27
  return createSolarite(...args)
16
28
  }
17
29
  });
18
30
 
19
-
20
31
  /** @type {HTMLElement|Class} */
21
32
  export {Solarite}
22
- export {default as r} from './r.js';
23
- export {getArg, ArgType} from './getArg.js';
24
- export {default as Template} from './Template.js';
25
33
  export {default as Globals} from './Globals.js';
34
+ export {default as SolariteUtil} from './Util.js';
26
35
 
27
- import Util from './Util.js';
28
- let getInputValue = Util.getInputValue;
29
- export {getInputValue};
30
- export {default as delve} from '../util/delve.js';
31
-
32
- //Experimental:
33
- //export {forEach, watchGet, watchSet} from './watch.js' // old, unfinished
34
- export {default as watch, renderWatched} from './watch3.js'; // unfinished
36
+ //export {default as watch, renderWatched} from './watch.js'; // unfinished
@@ -1,4 +1,4 @@
1
- import {assert} from "../util/Errors.js";
1
+ import {assert} from "./assert.js";
2
2
  import {getObjectHash, getObjectId} from "./hash.js";
3
3
  import Globals from "./Globals.js";
4
4
  import {RootNodeGroup} from "./NodeGroup.js";
@@ -18,19 +18,9 @@ export default class Template {
18
18
  /** @type {Array} Used for toJSON() and getObjectHash(). Stores values used to quickly create a string hash of this template. */
19
19
  hashedFields;
20
20
 
21
- /**
22
- * @deprecated
23
- * @type {ExprPath} Used with forEach() from watch.js
24
- * Set in ExprPath.apply() */
25
- parentPath;
26
-
27
21
  /** @type {NodeGroup} */
28
22
  nodeGroup;
29
23
 
30
- /**
31
- * @type {string[][]} */
32
- paths = [];
33
-
34
24
  /**
35
25
  *
36
26
  * @param htmlStrings {string[]}
@@ -81,17 +71,21 @@ export default class Template {
81
71
  if (standalone) {
82
72
  ng = new RootNodeGroup(this, null, options);
83
73
  el = ng.getRootNode();
84
- //Globals.nodeGroups.set(el, ng);
74
+ Globals.nodeGroups.set(el, ng); // Why was this commented out?
85
75
  firstTime = true;
86
76
  }
87
77
  else {
88
78
  ng = Globals.nodeGroups.get(el);
89
79
  if (!ng) {
90
80
  ng = new RootNodeGroup(this, el, options);
91
- //Globals.nodeGroups.set(el, ng);
81
+ Globals.nodeGroups.set(el, ng); // Why was this commented out?
92
82
  firstTime = true;
93
83
  }
94
- }
84
+
85
+ // This can happen if we try manually rendering one template to a NodeGroup that was created expecting a different template.
86
+ // These don't always have the same length, for example if one attribute has multiple expressions.
87
+ if (ng.paths.length === 0 && this.exprs.length || ng.paths.length > this.exprs.length)
88
+ 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.`); }
95
89
 
96
90
  // Creating the root nodegroup also renders it.
97
91
  // If we didn't just create it, we need to render it.
@@ -99,23 +93,32 @@ export default class Template {
99
93
  if (this.html?.length === 1 && !this.html[0])
100
94
  el.innerHTML = ''; // Fast path for empty component.
101
95
  else {
102
- ng.clearRenderWatched();
103
96
  ng.applyExprs(this.exprs);
104
97
  }
105
98
  }
106
99
 
100
+ ng.exprsToRender = new Map();
107
101
  return el;
108
102
  }
109
103
 
110
104
  getExactKey() {
111
- if (!this.exactKey)
112
- this.exactKey = getObjectHash(this); // calls this.toJSON().
105
+ if (!this.exactKey) {
106
+ if (this.exprs.length)
107
+ this.exactKey = getObjectHash(this);// calls this.toJSON().
108
+ else // Don't hash plain html.
109
+ this.exactKey = this.html[0];
110
+ }
113
111
  return this.exactKey;
114
112
  }
115
113
 
116
114
  getCloseKey() {
117
- if (!this.closeKey)
118
- this.closeKey = '@'+this.toJSON()[0];
115
+ //console.log(this.exprs.length)
116
+ if (!this.closeKey) {
117
+ if (this.exprs.length)
118
+ this.closeKey = /*'@' + */this.toJSON()[0];
119
+ else
120
+ this.closeKey = this.html[0];
121
+ }
119
122
  // Use the joined html when debugging? But it breaks some tests.
120
123
  //return '@'+this.html.join('|')
121
124