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/Shell.js CHANGED
@@ -1,8 +1,13 @@
1
- import {assert} from "./assert.js";
2
- import ExprPath, {ExprPathType, getNodePath} from "./ExprPath.js";
1
+ import assert from "./assert.js";
2
+ import Path from "./Path.js";
3
3
  import Util from "./Util.js";
4
4
  import Globals from "./Globals.js";
5
5
  import HtmlParser from "./HtmlParser.js";
6
+ import PathToEvent from "./PathToEvent.js";
7
+ import PathToAttribValue from "./PathToAttribValue.js";
8
+ import PathToAttribs from "./PathToAttribs.js";
9
+ import PathToNodes from "./PathToNodes.js";
10
+ import PathToComponent from "./PathToComponent.js";
6
11
 
7
12
  /**
8
13
  * A Shell is created from a tagged template expression instantiated as Nodes,
@@ -17,10 +22,10 @@ export default class Shell {
17
22
  * @type {DocumentFragment|Text} DOM parent of the shell's nodes. */
18
23
  fragment;
19
24
 
20
- /** @type {ExprPath[]} Paths to where expressions should go. */
25
+ /** @type {Path[]} Paths to where expressions should go. */
21
26
  paths = [];
22
27
 
23
- // Elements with events. Not yet used.
28
+ // Elements with events. Is there a reason to use this? We already mark event Exprs in Shell.js.
24
29
  // events = [];
25
30
 
26
31
  /** @type {int[][]} Array of paths */
@@ -32,14 +37,6 @@ export default class Shell {
32
37
  /** @type {int[][]} Array of paths */
33
38
  styles = [];
34
39
 
35
- /** @type {int[][]} Array of paths. Used by activateEmbeds() to quickly find components. */
36
- staticComponents = [];
37
-
38
- /** @type {{path:int[], attribs:Record<string, string>}[]} */
39
- //componentAttribs = [];
40
-
41
-
42
-
43
40
  /**
44
41
  * Create the nodes but without filling in the expressions.
45
42
  * This is useful because the expression-less nodes created by a template can be cached.
@@ -52,43 +49,52 @@ export default class Shell {
52
49
  this._html = html.join('');
53
50
  //#ENDIF
54
51
 
52
+ // If no html tags or entities, just create a text node.
55
53
  if (html.length === 1 && !html[0].match(/[<&]/)) {
56
- this.fragment = document.createTextNode(html[0]);
54
+ this.fragment = Globals.doc.createTextNode(html[0]);
57
55
  return;
58
56
  }
59
57
 
60
58
 
61
59
  // 1. Add placeholders
62
- let joinedHtml = Shell.addPlaceholders(html);
60
+ let htmlWithPlaceholders = Shell.addPlaceholders(html);
63
61
 
64
- let template = document.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
65
- if (joinedHtml)
66
- template.innerHTML = joinedHtml;
62
+ let template = Globals.doc.createElement('template'); // Using a single global template won't keep the nodes as children of the DocumentFragment.
63
+ if (htmlWithPlaceholders)
64
+ template.innerHTML = htmlWithPlaceholders;
67
65
  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(''))
66
+ template.content.append(Globals.doc.createTextNode(''))
69
67
  this.fragment = template.content;
70
68
 
71
69
  // 2. Find placeholders
72
70
  let node;
73
71
  let toRemove = [];
74
72
  let placeholdersUsed = 0;
75
- const walker = document.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
73
+ const walker = Globals.doc.createTreeWalker(this.fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT);
76
74
  while (node = walker.nextNode()) {
77
75
 
78
- // Remove previous after each iteration, so paths will still be calculated correctly.
76
+ // Remove previous elements after each iteration, so paths will still be calculated correctly.
79
77
  toRemove.map(el => el.remove());
80
78
  toRemove = [];
81
79
 
82
80
  // Replace attributes
83
81
  if (node.nodeType === 1) {
84
- for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes as we go.
82
+ const hasIs = node.hasAttribute('is');
83
+ const isComponent = (hasIs || node.tagName.includes('-'));
84
+ const componentAttribPaths = [];
85
+
86
+ for (let attr of [...node.attributes]) { // Copy the attributes array b/c we remove attributes with placeholders as we go.
85
87
 
86
88
  // Whole attribute
87
89
  let matches = attr.name.match(/^[\ue000-\uf8ff]$/)
88
90
  if (matches) {
89
- this.paths.push(new ExprPath(null, node, ExprPathType.AttribMultiple));
91
+ let path = new PathToAttribs(null, node);
92
+ this.paths.push(path);
93
+ if (isComponent)
94
+ componentAttribPaths.push(path);
95
+
90
96
  placeholdersUsed ++;
91
- node.removeAttribute(matches[0]);
97
+ node.removeAttribute(matches[0]); // TODO: Is this necessary?
92
98
  }
93
99
 
94
100
  // Just the attribute value.
@@ -96,22 +102,46 @@ export default class Shell {
96
102
  let parts = attr.value.split(/[\ue000-\uf8ff]/g);
97
103
  if (parts.length > 1) {
98
104
  let nonEmptyParts = (parts.length === 2 && !parts[0].length && !parts[1].length) ? null : parts;
99
- let type = Util.isEvent(attr.name) ? ExprPathType.Event : ExprPathType.AttribValue;
100
105
 
101
- this.paths.push(new ExprPath(null, node, type, attr.name, nonEmptyParts));
106
+ let path = Util.isEvent(attr.name)
107
+ ? new PathToEvent(null, node, attr.name, nonEmptyParts)
108
+ : new PathToAttribValue(null, node, attr.name, nonEmptyParts);
109
+ path.isHtmlProperty = Util.isHtmlProp(node, attr.name);
110
+ this.paths.push(path);
111
+ if (isComponent) {
112
+ path.isComponentAttrib = true;
113
+ componentAttribPaths.push(path);
114
+ }
115
+
102
116
  placeholdersUsed += parts.length - 1;
103
117
  node.setAttribute(attr.name, parts.join(''));
104
118
  }
105
119
  }
106
120
  }
121
+
122
+ // Web components
123
+ if (isComponent) {
124
+ let path = new PathToComponent(null, node);
125
+ path.attribPaths = componentAttribPaths;
126
+ this.paths.splice(this.paths.length - componentAttribPaths.length, 0, path); // Insert before its componentAttribPaths
127
+
128
+ if (hasIs) {
129
+ node.setAttribute('_is', node.getAttribute('is'));
130
+ node.removeAttribute('is');
131
+ }
132
+ }
107
133
  }
134
+
108
135
  // Replace comment placeholders
109
136
  else if (node.nodeType === 8 && node.nodeValue === '!✨!') {
110
137
 
138
+ if (node?.parentNode?.closest && node?.parentNode?.closest('[contenteditable]'))
139
+ throw new Error(`Contenteditable can't have expressions inside them. Use <div contenteditable value="\${...}"> instead.`);
140
+
111
141
  // Get or create nodeBefore.
112
142
  let nodeBefore = node.previousSibling; // Can be the same as another Path's nodeMarker.
113
143
  if (!nodeBefore) {
114
- nodeBefore = document.createComment('ExprPath:'+this.paths.length);
144
+ nodeBefore = Globals.doc.createComment('Path:'+this.paths.length);
115
145
  node.parentNode.insertBefore(nodeBefore, node)
116
146
  }
117
147
  /*#IFDEV*/assert(nodeBefore);/*#ENDIF*/
@@ -127,49 +157,48 @@ export default class Shell {
127
157
  // Re-use existing comment placeholder.
128
158
  else {
129
159
  nodeMarker = node;
130
- nodeMarker.textContent = 'ExprPathEnd:'+ this.paths.length;
160
+ nodeMarker.textContent = 'PathEnd:'+ this.paths.length;
131
161
  }
132
162
  /*#IFDEV*/assert(nodeMarker);/*#ENDIF*/
133
163
 
134
- let path = new ExprPath(nodeBefore, nodeMarker, ExprPathType.Content);
164
+ let path = new PathToNodes(nodeBefore, nodeMarker);
135
165
  this.paths.push(path);
136
166
  placeholdersUsed ++;
137
167
  }
138
168
 
169
+ // Comments become text nodes when inside textareas.
139
170
  else if (node.nodeType === 3 && node.parentNode?.tagName === 'TEXTAREA' && node.textContent.includes('<!--!✨!-->'))
140
171
  throw new Error(`Textarea can't have expressions inside them. Use <textarea value="\${...}"> instead.`);
141
-
142
172
 
143
173
 
144
174
  // Sometimes users will comment out a block of html code that has expressions.
145
175
  // Here we look for expressions in comments.
146
176
  // We don't actually update them dynamically, but we still add paths for them.
147
177
  // That way the expression count still matches.
148
- else if (node.nodeType === Node.COMMENT_NODE) {
178
+ else if (node.nodeType === 8) { // Node.COMMENT_NODE
149
179
  let parts = node.textContent.split(/[\ue000-\uf8ff]/g);
150
180
  for (let i=0; i<parts.length-1; i++) {
151
- let path = new ExprPath(node.previousSibling, node)
152
- path.type = ExprPathType.Comment;
181
+ let path = new Path(node.previousSibling, node)
153
182
  this.paths.push(path);
154
183
  placeholdersUsed ++;
155
184
  }
156
185
  }
157
186
 
158
187
  // Replace comment placeholders inside script and style tags, which have become text nodes.
159
- else if (node.nodeType === Node.TEXT_NODE && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) {
188
+ else if (node.nodeType === 3 && ['SCRIPT', 'STYLE'].includes(node.parentNode?.nodeName)) { // Node.TEXT_NODE
160
189
  let parts = node.textContent.split(commentPlaceholder);
161
190
  if (parts.length > 1) {
162
191
 
163
192
  let placeholders = [];
164
193
  for (let i = 0; i<parts.length; i++) {
165
- let current = document.createTextNode(parts[i]);
194
+ let current = Globals.doc.createTextNode(parts[i]);
166
195
  node.parentNode.insertBefore(current, node);
167
196
  if (i > 0)
168
197
  placeholders.push(current)
169
198
  }
170
199
 
171
200
  for (let i=0, node; node=placeholders[i]; i++) {
172
- let path = new ExprPath(node.previousSibling, node, ExprPathType.Content);
201
+ let path = new PathToNodes(node.previousSibling, node);
173
202
  this.paths.push(path);
174
203
  placeholdersUsed ++;
175
204
 
@@ -188,51 +217,31 @@ export default class Shell {
188
217
  if (placeholdersUsed !== html.length-1)
189
218
  throw new Error(`Could not parse expressions in template. Check for duplicate attributes or malformed html: ${html.join('${...}')}`);
190
219
 
191
- // Handle solarite-placeholder's.
192
-
193
- // 3. Rename "is" attributes so the Web Components don't instantiate until we have the values of their PathExpr arguments.
194
- // that happens in NodeGroup.applyComponentExprs()
195
- for (let el of this.fragment.querySelectorAll('[is]'))
196
- el.setAttribute('_is', el.getAttribute('is'));
197
-
198
220
  for (let path of this.paths) {
199
221
  if (path.nodeBefore)
200
222
  path.nodeBeforeIndex = Array.prototype.indexOf.call(path.nodeBefore.parentNode.childNodes, path.nodeBefore)
201
- path.nodeMarkerPath = getNodePath(path.nodeMarker)
202
223
 
203
- // Cache so we don't have to calculate this later inside NodeGroup.applyExprs()
204
- if (path.type === ExprPathType.AttribValue && path.nodeMarker.nodeType === 1 &&
205
- (path.nodeMarker.tagName.includes('-') || path.nodeMarker.hasAttribute('is'))) {
206
- path.type = ExprPathType.ComponentAttribValue;
207
- }
224
+ // Must be calculated after we remove the toRemove nodes:
225
+ path.nodeMarkerPath = Path.get(path.nodeMarker)
226
+
227
+
208
228
  }
209
229
 
210
230
  this.findEmbeds();
211
231
 
232
+
212
233
  /*#IFDEV*/this.verify();/*#ENDIF*/
213
234
  }
214
235
 
215
236
  /**
216
237
  * 1. Add a Unicode placeholder char for where expressions go within attributes.
217
238
  * 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.
239
+ * 3. Append -solarite-placeholder to the tag names of custom components so that we can instantiate them later
240
+ * when we can manually call their constructors with the proper attribute and children arguments from evaluated expressions.
219
241
  * @param htmlChunks {string[]}
220
- * @returns {string} */
242
+ * @returns {string} Html with the placeholders in place. */
221
243
  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
- }
244
+ let result = [];
236
245
 
237
246
  let htmlParser = new HtmlParser(); // Reset the context.
238
247
  for (let i = 0; i < htmlChunks.length; i++) {
@@ -240,10 +249,20 @@ export default class Shell {
240
249
 
241
250
  // Append -solarite-placholder to web component tags, so we can pass args to them when they're instantiated.
242
251
  let lastIndex = 0;
243
- let context = htmlParser.parse(lastHtml, (html, index, oldContext, newContext) => {
252
+ let context = htmlParser.parse(lastHtml, (html, index, prevContext/*, nextContext*/) => { // This function is called every time the html context changes.
244
253
  if (lastIndex !== index) {
245
254
  let token = html.slice(lastIndex, index);
246
- addToken(token, oldContext);
255
+
256
+ if (prevContext === HtmlParser.Tag) {
257
+ // Find Web Component tags and append -solarite-placeholder to their tag names
258
+ // This way we can gather their constructor arguments and their children before we call their constructor.
259
+ // Later, PathToComponent.apply() will replace them with the real components.
260
+ // Ctrl+F "solarite-placeholder" in project to find all code that manages subcomponents.
261
+ const isWebComponentTagName = /^<\/?[a-z][a-z0-9]*-[a-z0-9-]+/i; // a dash in the middle
262
+ token = token.replace(isWebComponentTagName, match => match + '-SOLARITE-PLACEHOLDER'); // caps to match other instances of this string, for better compression.
263
+ }
264
+
265
+ result.push(token);
247
266
  }
248
267
  lastIndex = index;
249
268
  });
@@ -251,13 +270,13 @@ export default class Shell {
251
270
  // Insert placeholders
252
271
  if (i < htmlChunks.length - 1) {
253
272
  if (context === HtmlParser.Text)
254
- tokens.push(commentPlaceholder) // Comment Placeholder. because we can't put text in between <tr> tags for example.
273
+ result.push(commentPlaceholder) // Comment Placeholder. because we can't put text in between <tr> tags for example.
255
274
  else
256
- tokens.push(String.fromCharCode(attribPlaceholder + i));
275
+ result.push(String.fromCharCode(attribPlaceholder + i));
257
276
  }
258
277
  }
259
278
 
260
- return tokens.join('');
279
+ return result.join('');
261
280
  }
262
281
 
263
282
  /**
@@ -269,10 +288,10 @@ export default class Shell {
269
288
  * this.ids
270
289
  * this.staticComponents */
271
290
  findEmbeds() {
272
- this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => getNodePath(el))
291
+ this.scripts = Array.prototype.map.call(this.fragment.querySelectorAll('scripts'), el => Path.get(el))
273
292
 
274
- // TODO: only find styles that have ExprPaths in them?
275
- this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => getNodePath(el))
293
+ // TODO: only find styles that have Paths in them?
294
+ this.styles = Array.prototype.map.call(this.fragment.querySelectorAll('style'), el => Path.get(el))
276
295
 
277
296
  let idEls = this.fragment.querySelectorAll('[id],[data-id]');
278
297
 
@@ -283,17 +302,7 @@ export default class Shell {
283
302
  throw new Error(`<${el.tagName.toLowerCase()} id="${id}"> can't override existing HTMLElement id property.`)
284
303
  }
285
304
 
286
- this.ids = Array.prototype.map.call(idEls, el => getNodePath(el))
287
-
288
- for (let el of this.fragment.querySelectorAll('*')) {
289
- if (el.tagName.includes('-') || el.hasAttribute('_is'))
290
-
291
- // Dynamic components are components that have attributes with expression values.
292
- // They are created from applyExprs()
293
- // But static components are created in a separate path inside the NodeGroup constructor.
294
- if (!this.paths.find(path => path.nodeMarker === el))
295
- this.staticComponents.push(getNodePath(el));
296
- }
305
+ this.ids = Array.prototype.map.call(idEls, el => Path.get(el))
297
306
  }
298
307
 
299
308
  /**
package/src/Solarite.d.ts CHANGED
@@ -1,24 +1,9 @@
1
1
  /**
2
- * Solarite JavasCript UI library.
2
+ * Solarite JavaScript UI library.
3
3
  * MIT License
4
4
  * https://vorticode.github.io/solarite/
5
5
  */
6
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
7
  export interface RenderOptions {
23
8
  styles?: boolean;
24
9
  scripts?: boolean;
@@ -26,37 +11,96 @@ export interface RenderOptions {
26
11
  render?: boolean;
27
12
  }
28
13
 
14
+ /**
15
+ * Tagged template literal or function for creating Templates and rendering to the DOM. */
16
+ declare function h(htmlStrings: TemplateStringsArray, ...exprs: any[]): Template;
17
+ declare function h(htmlStrings: string | string[], ...exprs: any[]): Template;
18
+ declare function h(el: HTMLElement | DocumentFragment, options?: RenderOptions): (htmlStrings: TemplateStringsArray, ...exprs: any[]) => HTMLElement | DocumentFragment;
19
+ declare function h(el: HTMLElement | DocumentFragment, template: Template, options?: RenderOptions): void;
20
+ declare function h(tag: string, props: object, ...children: any[]): Template; // JSX
21
+ declare function h(obj: {render: Function}): (htmlStrings: TemplateStringsArray, ...exprs: any[]) => void; // Rebound render
22
+
23
+ export default h;
24
+ export {h};
25
+ export {h as r};
26
+
27
+ /**
28
+ * Solarite provides more features if your web component extends Solarite instead of HTMLElement. */
29
+ export class Solarite extends HTMLElement {
30
+ constructor(attribs?: Record<string, any> | null);
31
+ render(attribs?: Record<string, any>, changed?:boolean): void;
32
+ renderFirstTime(): void;
33
+ connectedCallback(): void;
34
+ static define(tagName?: string | null): void;
35
+ static getAttribs(el: HTMLElement): Record<string, any>;
36
+ }
37
+
38
+
39
+ /**
40
+ * Convert a template, string, or object into a DOM Node or Element. */
41
+ export function toEl(arg: string | Template | {render: () => void}): Node | HTMLElement | DocumentFragment;
42
+
43
+ /**
44
+ * @deprecated
45
+ * Retrieve and cast an attribute value from an HTMLElement. */
46
+ export function getArg(el: HTMLElement, attributeName: string, defaultValue?: any,
47
+ type?: typeof ArgType[keyof typeof ArgType] | Function | any[]): any;
48
+
49
+ /**
50
+ * @deprecated
51
+ * Update attributes on an element from an object. */
52
+ export function setArgs(el: HTMLElement, args: object): void;
53
+
54
+ /** @deprecated */
55
+ export const ArgType: {
56
+ Bool: string;
57
+ Int: string;
58
+ Float: string;
59
+ String: string;
60
+ Json: string;
61
+ Eval: string;
62
+ }
63
+
29
64
  export class Template {
30
- exprs: (Template|string|Function)[];
65
+ exprs: any[];
31
66
  html: string[];
32
67
  constructor(htmlStrings: string[], exprs: any[]);
33
- render(el?: HTMLElement, options?: RenderOptions): DocumentFragment | HTMLElement | null;
68
+ render(el?: HTMLElement | null, options?: RenderOptions): HTMLElement | DocumentFragment;
34
69
  getExactKey(): string;
35
70
  getCloseKey(): string;
71
+ static fromJsx(tag: string, props: Record<string, any> | null, children: any[]): Template;
36
72
  }
37
73
 
38
74
  export function delve(obj: object, path: string[], createVal?: any): any;
39
75
 
40
-
41
-
42
- // Experimental:
43
- //--------------
44
-
45
- // Globals object
76
+ /**
77
+ * Internal utilities and state. */
46
78
  export const Globals: {
47
- componentArgsHash: WeakMap<any, any>;
48
79
  connected: WeakSet<HTMLElement>;
49
- currentExprPath: any;
80
+ currentPath: any;
81
+ currentSlotChildren: any[] | null;
50
82
  div: HTMLDivElement;
83
+ doc: Document;
51
84
  elementClasses: {[key: string]: typeof Node};
52
85
  htmlProps: {[key: string]: boolean};
53
86
  nodeEvents: WeakMap<Node, {[eventName: string]: [Function, Function, any[]]}>;
54
- nodeGroups: WeakMap<HTMLElement, any>;
87
+ rootNodeGroups: WeakMap<HTMLElement, any>;
55
88
  objToEl: WeakMap<any, any>;
56
89
  rendered: WeakSet<HTMLElement>;
57
- rendering: WeakSet<HTMLElement>;
58
90
  shells: WeakMap<string[], any>;
59
91
  reset: Function;
60
- count: number;
92
+ };
93
+
94
+ export const SolariteUtil: {
95
+ arraySame(a: any[], b: any[]): boolean;
96
+ attribsToObject(el: HTMLElement, ignore?: string | null): Record<string, any>;
97
+ bindId(root: any, el: HTMLElement): void;
98
+ bindStyles(style: HTMLStyleElement, root: HTMLElement): void;
99
+ camelToDashes(str: string): string;
100
+ dashesToCamel(str: string): string;
101
+ defineClass(Class: typeof HTMLElement, tagName?: string | null): void;
102
+ isIterable(obj: any): boolean;
103
+ trimEmptyNodes(nodes: NodeList | Node[]): Node[];
104
+ [key: string]: any;
61
105
  };
62
106
 
package/src/Solarite.js CHANGED
@@ -1,36 +1,148 @@
1
- /**
2
- * Solarite JavasCript UI library.
3
- * MIT License
4
- * https://vorticode.github.io/solarite/
5
- */
6
-
1
+ /*
2
+ ┏┓ ┓ •
3
+ ┗┓┏┓┃┏┓┏┓┓╋▗▖
4
+ ┗┛┗┛┗┗┻╹ ╹╹┗
5
+ JavasCript UI library
6
+ @license MIT
7
+ @copyright Vorticode LLC
8
+ https://vorticode.github.io/solarite/ */
7
9
  import h from './h.js';
8
10
  export default h;
9
- export {default as h, default as r} from './h.js'; //Named exports for h() are deprecated.
10
11
  export {default as delve} from './delve.js';
11
- export {getArg, ArgType} from './getArg.js';
12
12
  export {default as Template} from './Template.js';
13
-
14
-
13
+ export {default as toEl} from './toEl.js';
14
+ import Template from './Template.js';
15
15
 
16
16
  // Experimental:
17
17
  //--------------
18
+ export {default as Globals} from './Globals.js';
19
+ export {default as SolariteUtil} from './Util.js';
20
+
21
+ // Deprecated:
22
+ //--------------
23
+ export {default as h} from './h.js'; // Named exports for h() are deprecated.
24
+ export {getArg, ArgType} from './getArg.js';
18
25
  export {setArgs} from './getArg.js';
26
+ export {default as r} from './h.js';
27
+ export function t(html) {
28
+ return new Template([html], []);
29
+ }
30
+ //export {default as watch, renderWatched} from './watch.js'; // unfinished
31
+
32
+ // Only used for tests:
33
+ //--------------
34
+ export {default as HtmlParser} from './HtmlParser.js';
35
+ export {default as NodeGroup} from './NodeGroup.js';
36
+ export {default as Shell} from './Shell.js';
37
+
38
+
39
+
19
40
 
20
- import createSolarite from "./createSolarite.js";
41
+
42
+ // Solarite Class:
43
+ //--------------
44
+ import Util from "./Util.js";
45
+ import Globals from "./Globals.js";
21
46
 
22
47
  /**
23
- * TODO: The Proxy and the multiple base classes mess up 'instanceof Solarite'
24
- * @type {Node|Class<HTMLElement>|function(tagName:string):Node|Class<HTMLElement>} */
25
- const Solarite = new Proxy(createSolarite(), {
26
- apply(self, _, args) {
27
- return createSolarite(...args)
48
+ * Intercept the construct call to auto-define the class before the constructor is called. */
49
+ let HTMLElementAutoDefine = new Proxy(HTMLElement, {
50
+ construct(Parent, args, Class) {
51
+
52
+ // 1. Call customElements.define() automatically.
53
+ Util.defineClass(Class);
54
+
55
+ // 2. This line is equivalent the to super() call to HTMLElement:
56
+ return Reflect.construct(Parent, args, Class);
28
57
  }
29
58
  });
30
59
 
31
- /** @type {HTMLElement|Class} */
32
- export {Solarite}
33
- export {default as Globals} from './Globals.js';
34
- export {default as SolariteUtil} from './Util.js';
60
+ /**
61
+ * Solarite provides more features if your web component extends Solarite instead of HTMLElement.
62
+ *
63
+ * Reasons to inherit from Solarite instead of HTMLElement.
64
+ * 1. customElements.define() is called automatically when you create the first instance.
65
+ * 2. Calls render() when added to the DOM, if it hasn't been called already.
66
+ * 3. Populates the attribs argument to the constructor when instantiated from regular html outside a template string.
67
+ * It parses JSON from DOM attribute values surrouned with '${...}'
68
+ * 4. Shows an error if render() isn't defined.
69
+ *
70
+ * Advantages to inheriting from HTMLElement
71
+ * 1. We can inherit from things like HTMLTableRowElement directly.
72
+ * 2. There's less magic, since everyone is familiar with defining custom elements.
73
+ * 3. No confusion about how the class name becomes a tag name.
74
+ * @extends {HTMLElement} */
75
+ export class Solarite extends HTMLElementAutoDefine {
76
+
77
+ /**
78
+ * @param attribs {?Record<string, any>} */
79
+ constructor(attribs=null) {
80
+ super();
81
+
82
+ if (attribs) {
83
+ if (typeof attribs !== 'object')
84
+ throw new Error('First argument to custom element constructor must be an object.');
85
+
86
+ // 1. Populate attribs if it's an empty object.
87
+ if (attribs && !Object.keys(attribs).length) {
88
+ let attribs2 = Solarite.getAttribs(this);
89
+ for (let name in attribs2) {
90
+ attribs[name] = attribs2[name];
91
+ }
92
+ }
93
+
94
+ // 2. Populate fields from attribs.
95
+ // This does nothing because the fields are overwritten by the child class after this super() constructor executes.
96
+ //for (let name in attribs || {}) {
97
+ // if (name in this) {
98
+ // const descriptor = Object.getOwnPropertyDescriptor(this, name);
99
+ // if (!descriptor || descriptor.writable || descriptor.set)
100
+ // this[name] = attribs[name];
101
+ // }
102
+ //}
103
+ }
104
+
105
+ // 3. Wrap render function so it always provides the attribs argument.
106
+ // Disabled because this gives us strings for attribute values when we call render manually.
107
+ // Instead of values given from ${...} expressions.
108
+ // let originalRender = this.render;
109
+ // this.render = (attribs, changed=true) => {
110
+ // if (!attribs) // If we have to look up the attribs, we don't know if they changed or not.
111
+ // attribs = Solarite.getAttribs(this);
112
+ // originalRender.call(this, attribs, changed);
113
+ // }
114
+ }
35
115
 
36
- //export {default as watch, renderWatched} from './watch.js'; // unfinished
116
+ 'render'() {
117
+ throw new Error('render() is not defined for ' + this.constructor.name);
118
+ }
119
+
120
+ /**
121
+ * Call render() only if it hasn't already been called. */
122
+ 'renderFirstTime'() {
123
+ if (!Globals.rendered.has(this)) {
124
+ let attribs = Solarite.getAttribs(this);
125
+ this.render(attribs); // calls Globals.rendered.add(this); inside the call to h()'...'.
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Called automatically by the browser. */
131
+ 'connectedCallback'() { // quoted so terser doesn't remove it.
132
+ this.renderFirstTime();
133
+ }
134
+
135
+ static 'define'(tagName=null) {
136
+ Util.defineClass(this, tagName);
137
+ }
138
+
139
+ static 'getAttribs'(el) {
140
+ let result = Util.attribsToObject(el);
141
+ for (let name in result) {
142
+ let val = result[name];
143
+ if (val.startsWith('${') && val.endsWith('}'))
144
+ result[name] = JSON.parse(val.slice(2, -1));
145
+ }
146
+ return result;
147
+ }
148
+ }