solarite 0.1.1 → 0.2.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 (45) hide show
  1. package/benchmarks/naive/Solarite.min.js +4 -0
  2. package/benchmarks/naive/index.html +14 -0
  3. package/benchmarks/naive/main.js +339 -0
  4. package/benchmarks/naive/package-lock.json +13 -0
  5. package/benchmarks/naive/package.json +23 -0
  6. package/benchmarks/readme.md +48 -0
  7. package/build/build.bat +2 -2
  8. package/build/build.js +1 -1
  9. package/dist/Solarite-debug.js +1344 -1532
  10. package/dist/Solarite.js +1298 -1334
  11. package/dist/Solarite.min.js +3 -3
  12. package/dist/udomdiff-license.txt +18 -0
  13. package/docs/index.md +440 -130
  14. package/docs/js/Playground.js +1 -1
  15. package/docs/js/codemirror/codemirror6.js +3683 -3206
  16. package/docs/js/codemirror/themeSolarIce.js +1 -1
  17. package/docs/js/documentation.js +1 -1
  18. package/docs/js/ui/CodeEditor.js +183 -45
  19. package/docs/js/ui/FlexResizer.js +14 -3
  20. package/docs/js/util/Errors.js +11 -0
  21. package/docs/media/documentation.css +6 -3
  22. package/index.html +449 -30
  23. package/package.json +1 -1
  24. package/readme.md +1 -1
  25. package/src/solarite/ExprPath.js +349 -151
  26. package/src/solarite/Globals.js +43 -1
  27. package/src/solarite/NodeGroup.js +275 -293
  28. package/src/solarite/Shell.js +26 -28
  29. package/src/solarite/Solarite.js +12 -0
  30. package/src/solarite/Template.js +55 -128
  31. package/src/solarite/Util.js +131 -7
  32. package/src/solarite/createSolarite.js +14 -19
  33. package/src/solarite/getArg.js +1 -1
  34. package/src/solarite/hash.js +18 -35
  35. package/src/solarite/r.js +127 -127
  36. package/src/solarite/watch3.js +18 -11
  37. package/src/{solarite → unused}/NodeGroupManager.js +81 -109
  38. package/src/{solarite → unused}/watch.js +1 -1
  39. package/src/{solarite → unused}/watch2.js +1 -2
  40. package/src/{solarite → util}/MultiValueMap.js +18 -11
  41. package/src/util/WeakArray.js +33 -0
  42. package/tests/Solarite.test.js +862 -167
  43. package/tests/index.html +4 -2
  44. package/tests/run.bat +1 -1
  45. package/deno.lock +0 -176
package/docs/index.md CHANGED
@@ -1,26 +1,26 @@
1
1
  ---
2
2
  title: Solarite JS Library
3
- append-head: <script src="docs/js/ui/DarkToggle.js"></script><script type="module" src="docs/js/documentation.js"></script><link rel="stylesheet" href="docs/media/documentation.css"><link rel="stylesheet" href="docs/media/eternium.css"><link rel="icon" href="docs/media/solarite-machine.webp" type="image/webp">
3
+ append-head: <script src="docs/js/ui/DarkToggle.js"></script><script type="module" src="docs/js/documentation.js"></script><link rel="stylesheet" href="docs/media/documentation.css"><link rel="stylesheet" href="docs/media/eternium.css"><link rel="icon" href="docs/media/solarite-machine.webp" type="image/webp"><script async defer src="https://buttons.github.io/buttons.js"></script>
4
4
 
5
5
  ---
6
6
 
7
- <!-- To convert documentation to html: (1) Open in Typora. (2) Select the GitHub theme. (3) Export as html with styles to index.html. -->
7
+ <!-- To convert documentation to html: (1) Open in Typora. (2) Select the GitHub theme, or go to Settings -> Export -> Html -> Theme -> Github. (3) Export as html with styles to index.html. -->
8
+
9
+ <!-- Playgrounds that don't have a lowercase language name will not have a preview. -->
8
10
 
9
11
  # Solarite
10
12
 
11
- Solarite is a small (8KB min+gzip), fast, compilation-free JavaScript library to enhance your vanilla web components. Features:
13
+ Solarite is a small (8KB min+gzip), fast, compilation-free JavaScript library for creating elements and web components. Features:
12
14
 
13
- - Very similar to writing native Web Components.
14
15
  - Minimal DOM updates when rendering.
15
- - No magic: Rendering only when you want it, via the manually invoked render() method.
16
+ - No magic: Renderer only when you want via the manually invoked `render()` method.
17
+ - No setting up state variables. Render any regular variable or data structure.
16
18
  - Local scoped styles: Inherit external styles but define new styles that apply only to the web component and its children.
17
19
  - Elements with `id` or `data-id` attributes become class properties.
18
- - Attributes are passed as constructor arguments to nested Solarite components.
20
+ - Attributes are passed as constructor arguments to nested components.
19
21
  - Single file. No build steps and no dependencies. Not even Node.js. Just `import` Solarite.js or Solarite.min.js into your vanilla JavaScript and start coding.
20
22
  - MIT license. Free for commercial use. No attribution needed.
21
23
 
22
- With Solarite there's no need to set up state like with other frameworks. Instead, use any regular variables or data structures in html templates. Call `render()` manually and it will update changed elements synchronously.
23
-
24
24
  ```javascript
25
25
  import {r} from './dist/Solarite.js';
26
26
 
@@ -55,16 +55,10 @@ class ShoppingList extends HTMLElement {
55
55
 
56
56
  ${this.items.map(item => r`
57
57
  <div>
58
- <input placeholder="Name" value=${item.name}
59
- oninput=${e => {
60
- item.name = e.target.value;
61
- this.render()
62
- }}>
63
- <input type="number" value=${item.qty}
64
- oninput=${e => {
65
- item.qty = e.target.value;
66
- this.render()
67
- }}>
58
+ <input placeholder="Item" oninput=${this.render}
59
+ value=${[item, 'name']}> <!-- 2-way binding -->
60
+ <input type="number" oninput=${this.render}
61
+ value=${[item, 'qty']}>
68
62
  <button onclick=${()=>this.removeItem(item)}>x</button>
69
63
  </div>
70
64
  `)}
@@ -78,26 +72,71 @@ customElements.define('shopping-list', ShoppingList);
78
72
  document.body.append(new ShoppingList()); // add <shopping-list> element
79
73
  ```
80
74
 
81
- This project is currently in BETA stage and not yet recommended for production code.
82
-
83
75
  To use, import one of these pre-bundled es6 modules into your project:
84
76
 
85
- - [Solarite.js](https://cdn.jsdelivr.net/gh/Vorticode/Solarite/dist/Solarite.js) - 87KB
86
- - [Solarite.min.js](https://cdn.jsdelivr.net/gh/Vorticode/Solarite/dist/Solarite.js) - 24KB / 8KB gzipped
77
+ - [Solarite.js](https://cdn.jsdelivr.net/gh/Vorticode/Solarite/dist/Solarite.js) - 85KB
78
+ - [Solarite.min.js](https://cdn.jsdelivr.net/gh/Vorticode/Solarite/dist/Solarite.min.js) - 24KB / 8KB gzipped
87
79
 
88
80
  Or get Solarite from GitHub or NPM:
89
81
 
90
- - [Solarite GitHub Repository](https://github.com/Vorticode/solarite)
82
+ - [Solarite GitHub Repository](https://github.com/Vorticode/solarite) <a class="github-button" href="https://github.com/vorticode/solarite" data-color-scheme="no-preference: light; light: light; dark: dark;" data-icon="octicon-star" data-size="small" data-show-count="true" aria-label="Star vorticode/solarite on GitHub">Star</a>
91
83
  - `git clone https://github.com/Vorticode/solarite.git`
92
84
  - `npm install solarite`
93
85
 
86
+ This project is currently in BETA stage. Tip: A JetBrains IDE like [WebStorm](https://www.jetbrains.com/webstorm/), [PhpStorm](https://www.jetbrains.com/phpstorm/), or [IDEA](https://www.jetbrains.com/idea/) will syntax highlight the html template strings.
87
+
94
88
  ## Concepts
95
89
 
90
+ ### Regular Elements
91
+
92
+ The `r()` function can create elements. Pass any object with a `render()` function as the first argument. This object can optionally have additional properties and methods, which become bound to the resulting element. When `render()` is called, only the changed nodes will be updated.
93
+
94
+ ```javascript
95
+ import {r} from './dist/Solarite.js';
96
+
97
+ let button = r({
98
+ count: 0,
99
+
100
+ inc() {
101
+ this.count++;
102
+ this.render();
103
+ },
104
+
105
+ render() {
106
+ r(this)`<button onclick=${this.inc}>I've been clicked ${this.count} times.</button>`
107
+ }
108
+ });
109
+ document.body.append(button);
110
+ ```
111
+
112
+ If you want multiple instances of such an element, the code above can be wrapped in a function:
113
+
114
+ ```javascript
115
+ import {r} from './dist/Solarite.js';
116
+
117
+ function createButton(text) {
118
+ return r({
119
+ count: 0,
120
+
121
+ inc() {
122
+ this.count++;
123
+ this.render();
124
+ },
125
+
126
+ render() {
127
+ r(this)`<button onclick=${this.inc}>${this.count} ${text}</button>`
128
+ }
129
+ })
130
+ }
131
+ document.body.append(createButton('clicks'));
132
+ document.body.append(createButton('tickles'));
133
+ ```
134
+
96
135
  ### Web Components
97
136
 
98
- In this minimal example, we make a new class called `MyComponent` which extends from `HTMLElement` like other web components. We provide a `render()` function to set its html, and a constructor to call it when a new instance is created.
137
+ Solarite can also create [web components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components). In this minimal example, we make a new class called `MyComponent` which extends from `HTMLElement` like any other web component. We provide a `render()` function to set its html, and a constructor to call it when a new instance is created.
99
138
 
100
- All browsers require custom web component tag names to have at least one dash in the middle.
139
+ All browsers require web component tag names to have at least one dash in the middle.
101
140
 
102
141
  ```javascript
103
142
  import {r} from './dist/Solarite.js';
@@ -106,34 +145,42 @@ class MyComponent extends HTMLElement {
106
145
  name = 'Solarite';
107
146
 
108
147
  constructor() {
109
- super();
148
+ super(); // JavaScript requires a super() call for sub-class construtors.
110
149
  this.render();
111
150
  }
112
151
 
113
- render() {
114
- r(this)`<my-component>Hello <b>${this.name}!<b></my-component>`
152
+ render() {
153
+ // This is how we'd create a web component using vanilla JavaScript
154
+ // without Solarite. But this recreates all children on every render!
155
+ //this.innerHTML = `Hello <b>${this.name}!<b>`;
156
+
157
+ // Using Solarite's r() function performs minimal updates on render.
158
+ r(this)`<my-component>Hello <b>${this.name}!</b></my-component>`
115
159
  }
116
160
  }
117
161
 
162
+ // Register the <my-component> tag name with the browser.
163
+ // Browsers require this for all web components.
118
164
  customElements.define('my-component', MyComponent);
165
+
119
166
  document.body.append(new MyComponent());
120
167
  ```
121
168
 
122
169
  Alternatively, instead of instantiating the element in JavaScript, we could can instantiate the element directly from html:
123
170
 
124
- ```html
171
+ ```Html
125
172
  <my-component></my-component>
126
173
  ```
127
174
 
128
- JavaScript veterans will realize that other than the `r()` function, this is highly similar to one might create vanilla JavaScript web components. This is by design!
175
+ JavaScript veterans will realize that other than the `r()` function, this is highly similar to one might create vanilla JavaScript web components. This is by design!
129
176
 
130
- Tip: A JetBrains IDE like [WebStorm](https://www.jetbrains.com/webstorm/), [PhpStorm](https://www.jetbrains.com/phpstorm/), or [IDEA](https://www.jetbrains.com/idea/) will syntax highlight the html template strings.
177
+ Since these are just regular web components, they can define the [connectedCallback()](https://developer.salesforce.com/docs/platform/lwc/guide/create-lifecycle-hooks-dom.html#connectedcallback) and [disconnectedCallback()](https://developer.salesforce.com/docs/platform/lwc/guide/create-lifecycle-hooks-dom.html#disconnectedcallback) methods that will be called when they're added and removed from the DOM, respectively. These functions are only supported for web components and not regular elements.
131
178
 
132
- ### render() and r()
179
+ ### Rendering
133
180
 
134
- The `r` function, when used as part of a [tagged template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) , converts the html and embedded expressions into a Solarite `Template`. This is a data structure used by Solarite to store processed html and expressions. The call to `r(this)` then renders that `Template` as web component's attributes and children. You can think of this like assigning to the browser's built-in `this.outerHTML` property, except updates are much faster because only the changed elements are replaced, instead of all nodes.
181
+ The `r` function, when used as a [tagged template literal](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) , converts the html and embedded expressions into a Solarite `Template`. This is a data structure used by Solarite to store processed html and expressions. The call to `r(this)` then renders that `Template` as an element's attributes and children. You can think of this like assigning to the browser's built-in `this.outerHTML` property, except updates are much faster because only the changed elements are replaced, instead of all nodes.
135
182
 
136
- Unlike other frameworks, Solarite does not re-render automatically when data changes, so you should call the `render()` function manually. This is a deliberate design choice to reduce magic, since in some cases you may want to update internal data without rendering.
183
+ Unlike other frameworks, Solarite does not re-render automatically when data changes, so you should call the `render()` function manually. This is a deliberate design choice to reduce unexpected side effects, since in some cases you may want to update internal data without rendering.
137
184
 
138
185
  Wrapping the web component's html in its tag name is optional. But without it you then must set any attributes on your web component manually, as seen in this example:
139
186
 
@@ -155,42 +202,47 @@ customElements.define('my-component', MyComponent);
155
202
  document.body.append(new MyComponent());
156
203
  ```
157
204
 
158
- If you do wrap the components html in its tag, that tag name must exactly match the tag name passed to customElements.define().
159
-
160
- ### Loops
205
+ If you do wrap the web component's html in its tag, that tag name must exactly match the tag name passed to `customElements.define()`.
161
206
 
162
- As previously seen, loops can be written with JavaScript's [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) function:
207
+ Note that by default, `r()` will render expressions as text, with escaped html entities. To render as html, wrap a variable in the `r()` function:
163
208
 
164
209
  ```javascript
165
210
  import {r} from './dist/Solarite.js';
166
211
 
167
- class TodoList extends HTMLElement {
168
- render() {
169
- r(this)`
170
- <todo-list>
171
- ${this.items.map(item =>
172
- r`${item}<br>`
173
- )}
174
- </todo-list>`
212
+ let folderIcon = `
213
+ <svg width="10em" height="10em" viewBox="0 0 24 24">
214
+ <path fill="currentColor" d="M2 4h8l2 2h10v14H2V4Zm2 2v12h16V8h-8.825l-2-2H4Zm0 12V6v12Z"/>
215
+ </svg>`;
216
+
217
+
218
+ let icon1 = r({
219
+ render() {
220
+ r(this)`<div>${folderIcon}</div>`
175
221
  }
176
- }
177
- customElements.define('todo-list', TodoList);
222
+ });
223
+ document.body.append(icon1);
178
224
 
179
- let list = new TodoList();
180
- list.items = ['one', 'two', 'three'];
181
- list.render();
182
- document.body.append(list);
183
225
 
184
- list.items[1] = '2';
185
- list.render();
226
+ let icon2 = r({
227
+ render() { // string wrapped in r()
228
+ r(this)`<div>${r(folderIcon)}</div>`
229
+ }
230
+ });
231
+ document.body.append(icon2);
186
232
 
187
- list.items.splice(1, 0, 'two and a half');
188
- list.render();
189
233
  ```
190
234
 
191
- When we change an element or add another element to the `items` list, calling `render()` only redraws the changed or new element. The other list items are not modified.
235
+ Folder icon comes from [Google](https://icon-sets.iconify.design/material-symbols/folder-outline/).
192
236
 
193
- Note that nested template literals must also have the `r` prefix. Otherwise they'll be rendered as escaped text instead of HTML elements.
237
+ These types of objects can be returned by in expressions with `r` tagged template literals:
238
+
239
+ 1. strings and numbers.
240
+ 2. boolean true, which will be rendered as 'true'
241
+ 3. false, null, and undefined, which will be rendered as empty string.
242
+ 4. Solarite Templates created by `r`-tagged template literals.
243
+ 5. DOM Nodes, including other web components.
244
+ 6. Arrays of any of the above.
245
+ 7. Functions that return any of the above.
194
246
 
195
247
  ### Attributes
196
248
 
@@ -203,34 +255,57 @@ let style = 'width: 100px; height: 40px; background: orange';
203
255
  let isEditable = true;
204
256
  let height = 40;
205
257
 
206
- class AttributeDemo extends HTMLElement {
258
+ let attributeDemo = r({
207
259
  render() {
208
260
  r(this)`
209
- <attribute-demo class="big">
210
-
261
+ <div class="big">
211
262
  <div style=${style}>Look at me</div>
212
-
213
263
  <div style="${'width: 100px'}; height: ${height}px; background: gray">Look at me</div>
264
+ <div style="width: 100px; height: 40px; background: brown" ${'title="I have a title"'}>Hover me</div>
265
+ <div style="width: 100px; height: 40px; background: red" contenteditable=${isEditable} >Edit me</div>
266
+ </div>`
267
+ }
268
+ });
214
269
 
215
- <div style="width: 100px; height: 40px; background: red" ${'title="I have a title"'}>Hover me</div>
270
+ document.body.append(attributeDemo);
216
271
 
217
- <div style="width: 100px; height: 40px; background: brown" contenteditable=${isEditable} >Edit me</div>
218
- </attribute-demo>`
219
- }
220
- }
221
- customElements.define('attribute-demo', AttributeDemo);
222
- let ad = new AttributeDemo();
223
- ad.render();
224
- document.body.append(ad);
272
+ style = 'width: 100px; height: 40px; background: green';
273
+ setTimeout(attributeDemo.render, 2000);
225
274
  ```
226
275
 
227
276
  Expressions can also toggle the presence of an attribute. In the last div above, if `isEditable` is false, null, or undefined, the contenteditable attribute will be removed.
228
277
 
229
278
  Note that attributes can also be assigned to the root element, such as `class="big"` on the `<attribute-demo>` tag above.
230
279
 
280
+ ### Id's
281
+
282
+ Any element in the html with an `id` or `data-id` attribute is automatically bound to a property with the same name on the class instance. But this only happens after `render()` is first called:
283
+
284
+ ```javascript
285
+ import {r} from './dist/Solarite.js';
286
+
287
+ let raceTeam = r({
288
+ render() {
289
+ r(this)`
290
+ <div>
291
+ <input id="driver" value="Mario">
292
+ <div data-id="car">Cutlas Supreme</div>
293
+ <div data-id="instructor.name">Lightning McQueen</div>
294
+ </div>`
295
+ }
296
+ });
297
+ document.body.append(raceTeam);
298
+
299
+ raceTeam.driver.value = 'Luigi';
300
+ raceTeam.car.style.border = '1px solid green';
301
+ // We don't need to call render() because we're editing the DOM Directly.
302
+ ```
303
+
304
+ Id's that have values matching built-in HTMLElement attribute names such as `title` or `disabled` are not allowed.
305
+
231
306
  ### Events
232
307
 
233
- Listen for events by assigning a function expression to any event attribute. Or by passing an array where the first item is a function and subsequent items are arguments to that function.
308
+ To intercept events, set the value of an event attribute like `onclick` to a function. Alternatively, set the value to an array where the first item is a function and subsequent items are arguments to that function.
234
309
 
235
310
  ```javascript
236
311
  import {r} from './dist/Solarite.js';
@@ -257,13 +332,13 @@ customElements.define('event-demo', EventDemo);
257
332
  document.body.append(new EventDemo());
258
333
  ```
259
334
 
260
- Event binding with an array containing a function and its arguments is slightly faster, since when `render()` is called, Solarite can see that the function hasn't changed, and it doesn't need to be unbound and rebound. But the performance difference is usually negligible.
335
+ Event binding with an array containing a function and its arguments is slightly faster, since the function isn't recreated when `render()` is called, and it doesn't need to be unbound and rebound. But the performance difference is usually negligible.
261
336
 
262
337
  Make sure to put your events inside `${...}` expressions, because classic events can't reference variables in the current scope.
263
338
 
264
339
  ### Two-Way Binding
265
340
 
266
- Form elements can update the properties that provide their values if an event attribute such as `oninput` is assigned the path to a property to update:
341
+ Form elements can update the properties that provide their values if an event attribute such as `oninput` is assigned a function to perform the update:
267
342
 
268
343
  ```javascript
269
344
  import {r} from './dist/Solarite.js';
@@ -297,46 +372,75 @@ customElements.define('binding-demo', BindingDemo);
297
372
  document.body.append(new BindingDemo());
298
373
  ```
299
374
 
300
- In addition to `<input>`, `<select>` and `<textarea>` can also use the `value` attribute to set their value on render. Likewise so can any custom web components that define a `value` property.
375
+ In addition to `<input>`, `<select>` and `<textarea>` can also use the `value` attribute to set their value on render. Likewise so can any custom web component that defines a `value` property.
301
376
 
302
- ### Id's
303
-
304
- Any element in the html with an `id` or `data-id` attribute is automatically bound to a property with the same name on the class instance. But this only happens after `render()` is first called:
377
+ A shorthand way to do two-way binding is to pass a property path as the value expression. Here with `value=${[this, 'count']}`. When a user types in the input, Solarite listens to the `oninput` listener and updates `this.count`. Note that we still need a second `oninput` attribute if we want to trigger rendering.
305
378
 
306
379
  ```javascript
307
380
  import {r} from './dist/Solarite.js';
308
381
 
309
- class RaceTeam extends HTMLElement {
310
- constructor() {
382
+ class BindingDemo extends HTMLElement {
383
+
384
+ constructor() {
311
385
  super();
312
-
313
- // Id's are not set until render() is first called.
386
+ this.count = 0;
314
387
  this.render();
315
-
316
- // Change the value of the input.
317
- // No need to render() again since we're changing the DOM manually.
318
- this.driver.value = 'Luigi';
319
388
  }
320
389
 
321
390
  render() {
322
- r(this)`
323
- <race-team>
324
- <input id="driver" value="Mario">
325
- <div data-id="car">Cutlas Supreme</div>
326
- <div data-id="instructor.name">Lightning McQueen</div>
327
- </race-team>`
391
+ r(this)`
392
+ <binding-demo>
393
+ <input type="number" value=${[this, 'count']}
394
+ oninput=${() => this.render()}>
395
+ <pre>count is ${this.count}</pre>
396
+ <button onclick=${()=> {
397
+ this.count = 0;
398
+ this.render();
399
+ }}>Reset</button>
400
+ </binding-demo>`
328
401
  }
329
402
  }
330
- customElements.define('race-team', RaceTeam);
331
- let raceTeam = new RaceTeam();
332
- document.body.append(raceTeam);
403
+ customElements.define('binding-demo', BindingDemo);
404
+
405
+ document.body.append(new BindingDemo());
406
+ ```
333
407
 
334
408
 
335
- raceTeam.car.style.border = '1px solid green';
336
409
 
410
+ ### Loops
411
+
412
+ As previously seen, loops can be written with JavaScript's [Array.map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map) function:
413
+
414
+ ```javascript
415
+ import {r} from './dist/Solarite.js';
416
+
417
+ class TodoList extends HTMLElement {
418
+ render() {
419
+ r(this)`
420
+ <todo-list>
421
+ ${this.items.map(item =>
422
+ r`${item}<br>`
423
+ )}
424
+ </todo-list>`
425
+ }
426
+ }
427
+ customElements.define('todo-list', TodoList);
428
+
429
+ let list = new TodoList();
430
+ list.items = ['one', 'two', 'three'];
431
+ list.render();
432
+ document.body.append(list);
433
+
434
+ list.items[1] = '2';
435
+ list.render();
436
+
437
+ list.items.splice(1, 0, 'two and a half');
438
+ list.render();
337
439
  ```
338
440
 
339
- Id's that have values matching built-in HTMLElement attribute names such as `title` or `disabled` are not allowed.
441
+ When we change an element or add another element to the `items` list, calling `render()` only redraws the changed or new element. The other list items are not modified.
442
+
443
+ Note that nested template literals must also have the `r` prefix. Otherwise they'll be rendered as escaped text instead of HTML elements.
340
444
 
341
445
  ### Scoped Styles
342
446
 
@@ -361,7 +465,7 @@ class FancyText extends HTMLElement {
361
465
  <p>I have a red border and shadow!</p>
362
466
  </fancy-text>`
363
467
 
364
- /* This is rewritten as:
468
+ /* The code above is rewritten as:
365
469
  <fancy-text data-style="1">
366
470
  <style>
367
471
  fancy-text[data-style="1"] { display: block; border: 10px dashed red }
@@ -380,9 +484,9 @@ document.body.append(el);
380
484
 
381
485
  Note that if shadow-dom is used, the element will not replace the `:host` selector in styles, as browsers natively support the `:host` selector when inside shadow DOM.
382
486
 
383
- ### Sub Components
487
+ ### Child Components
384
488
 
385
- When one Solarite component is embedded within another, its attributes and children are passed as arguments to the constructor:
489
+ When one web component is embedded within another, its attributes and children are passed as arguments to the constructor:
386
490
 
387
491
  ```javascript
388
492
  import {r} from './dist/Solarite.js';
@@ -395,7 +499,10 @@ class NotesItem extends HTMLElement {
395
499
  this.render();
396
500
  }
397
501
 
398
- render() {
502
+ render({item}={}) {
503
+ // If item passed to the constructor has changed.
504
+ if (item)
505
+ this.item = item;
399
506
  r(this)`
400
507
  <notes-item>
401
508
  <b>${this.item.name}</b> - ${this.item.description}<br>
@@ -430,35 +537,79 @@ list.items = [
430
537
  ]
431
538
  list.render();
432
539
  document.body.append(list);
540
+
541
+ list.items[0].name = 'PhysEd';
542
+
543
+ // list.items[0] has changed,
544
+ // so this will call render() on the first NotesItem,
545
+ // passing the new item object to its render() function.
546
+ list.render();
547
+
433
548
  ```
434
549
 
435
- Note that calling `render()` on a parent component will call it on sub-components too.
550
+ Calling `render()` on a parent component will call `render()` on child components if the attributes passed to the child component have changed. The new attributes will be passed as an object as the first argument to the child component's `render()` function. The `render()` function can then decide what to do with that data, and if it should re-render itself by calling `r(this)`, which will in turn call `render()` on its own child web components.
436
551
 
437
- ### Classless Elements
552
+ In the above code, we alternatively could've created the `<notes-item>` element via the `new` keyword, but doing so would cause all `NotesItem` components to be recreated on every render.
438
553
 
439
- The `r()` function can also create elements outside of a class. Pass a function that returns a `Template` as the first argument. Optionally set the second argument to an object of additional properties and methods.
554
+ ```JavaScript
555
+ class NotesList extends HTMLElement {
556
+ render() {
557
+ r(this)`
558
+ <notes-list>
559
+ ${this.items.map(item => // Pass item object to NotesItem constructor:
560
+ new NotesItem({item: item})
561
+ )}
562
+ </notes-list>`
563
+ }
564
+ }
565
+ ```
440
566
 
441
- ```javascript
567
+
568
+
569
+ ### The r() function
570
+
571
+ The `r()` function renders templates and elements. There are multiple ways to use the `r()` function:
572
+
573
+ ```JavaScript
442
574
  import {r} from './dist/Solarite.js';
443
575
 
444
- let count = 0;
445
- let button = r(
446
- // Render function
447
- () => r`
448
- <button onclick=${(ev, self) => {
449
- count++;
450
- self.render();
451
- }}>I've been clicked ${count} times</button>`,
452
-
453
- // User-defined function
454
- {
455
- inc() {
456
- count++;
457
- this.render();
458
- }
459
- }
460
- );
576
+ // r`string`
577
+ // Convert the html to a Template that can later be used to create nodes.
578
+ let template = r`Hello ${"World"}!`;
579
+
580
+ // r(HTMLElement, Template)
581
+ // Render the template created by #1 to the <body> tag.
582
+ r(document.body, template);
583
+
584
+ // r(Template):Node|HTMLElement
585
+ // Render Template created by #1 creating a standaline HTML Element
586
+ let el = r(template);
587
+
588
+ // r(HTMLElement)`string`
589
+ // Create template and render its nodes to el.
590
+ r(el)`<b>${'Hi'}</b>`;
591
+
592
+ // r(html:string):TextNode
593
+ // Create single text node.
594
+ let textNode = r('Hello');
595
+
596
+ // r(html:string):HTMLElement
597
+ // Create single HTMLElement.
598
+ let el2 = r('<b>Hello</b>');
599
+
600
+ // r(html:string):DocumentFragment
601
+ // Create document fragment because there's more than one node.
602
+ let fragment = r('Hello <u>Goodbye</u>');
603
+
604
+ // r(function():Template, Object<string, function|*>):HTMLElement
605
+ // Crete a button element, with the fist function being the render function.
606
+ let button = r({
607
+ render() {
608
+ r(this)`<button>Submit</button>`
609
+ }
610
+ });
461
611
  document.body.append(button);
612
+
462
613
  ```
463
614
 
464
615
  ### Extending Other DOM Elements.
@@ -492,6 +643,70 @@ for (let i=0; i<10; i++) {
492
643
  document.body.append(table);
493
644
  ```
494
645
 
646
+ ### Manual DOM Ops
647
+
648
+ You can perform manual DOM operations on your elements in these cases:
649
+
650
+ 1. Modify any attributes that are not created by expressions, on any nodes not created by expressions.
651
+ 2. Add/remove nodes that:
652
+ 1. Are not created by an expression
653
+ 2. Are not directly before or after an expression that creates nodes.
654
+ 3. Do not have any attributes created by expressions.
655
+ 3. Modify any node, as long as you restore its previous position and attributes before `render()` is called again.
656
+
657
+ This example creates a list inside a `div` element and demonstrates which manual DOM operations are allowed.
658
+
659
+ ```javascript
660
+ import {r} from './dist/Solarite.js';
661
+
662
+ let list = r({
663
+ items: [],
664
+
665
+ add() {
666
+ this.items.push('Item ' + this.items.length);
667
+ this.render();
668
+ },
669
+
670
+ render() {
671
+ r(this)`<div>
672
+ <button onclick=${this.add}>Add Item</button>
673
+ <hr>
674
+ ${this.items.map(item => r`
675
+ <p>${item}</p>
676
+ `)}
677
+ </div>`
678
+ }
679
+ });
680
+
681
+ document.body.append(list);
682
+
683
+ // Set attributes not created by expressions. This is allowed.
684
+ list.setAttribute('title', 'DOM manipuulation demo');
685
+ list.querySelector('button').setAttribute('title', 'Click me');
686
+
687
+ // Remove the <hr> element.
688
+ // This is fine, because the hr element isn't part of an expression.
689
+ // And isn't adjacent to an expression, because there's a whitespace
690
+ // node between the <hr> and the expression.
691
+ // You could also put a comment node between them.
692
+ list.querySelector('hr').remove();
693
+ list.render();
694
+
695
+ // Remove the first <p> element and add it back again.
696
+ // This is fine, because we put it back the way it was before render()
697
+ list.add();
698
+ let p = list.querySelector('p');
699
+ list.append(p); // put it back.
700
+ list.render();
701
+
702
+ // Remove the first <p> element.
703
+ // This will cause an error because we're modifying nodes created by an expression.
704
+ // list.querySelector('p').remove();
705
+ // list.render();
706
+ ```
707
+
708
+
709
+
495
710
  ### The Solarite Class (Experimental)
496
711
 
497
712
  Instead of inheriting from HTMLElement, you can inherit from the `Solarite` class, which will add a little bit of magic to your web component:
@@ -546,8 +761,8 @@ class MyTasks extends HTMLElement {
546
761
  <div>
547
762
  ${this.tasks.map((task, index) => r`
548
763
  <div>
549
- ${task.text}
550
- <button onClick=${() => this.deleteTask(index)}>Delete</button>
764
+ ${task}
765
+ <button onclick=${() => this.deleteTask(index)}>Delete</button>
551
766
  </div>`
552
767
  )}
553
768
  </div>`;
@@ -557,17 +772,112 @@ customElements.define('my-tasks', MyTasks);
557
772
 
558
773
  let myTasks = new MyTasks();
559
774
  for (let i=0; i<10; i++)
560
- myTasks.tasks.push({
561
- text: 'Item ' + i,
562
- completed: false
563
- });
775
+ myTasks.tasks.push('Item ' + i);
564
776
  myTasks.render();
565
777
  document.body.append(myTasks);
566
778
  ```
567
779
 
568
780
  When `render()` is called:
569
781
 
570
- 1. Solarite's `r()` function gets the array of raw strings created by tasks.map() using a template literal function.
571
- 2. It then creates a hash of the values of each item in the array.
782
+ 1. Solarite's `r()` function gets the array of raw strings created by `tasks.map()` using a template literal function.
783
+ 2. It then creates a hash of the value of every `${...}` expression given to the `r()` function.
572
784
  3. Then it compares those hashes with the hashes from the last time rendering happened, and only update elements and attributes given values that have changed.
573
785
 
786
+ Solarite uses [WebReflection/udomdiff](https://github.com/WebReflection/udomdiff) to compare DOM nodes to update.
787
+
788
+ ## Examples
789
+
790
+ This is the time example from Lit.js implemented with Solarite:
791
+
792
+ ```html
793
+ <script type="module">
794
+ import {r, getArg} from './dist/Solarite.js';
795
+
796
+ const replay = r`<svg enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><title>Replay</title><g><rect fill="none" height="24" width="24"/><rect fill="none" height="24" width="24"/><rect fill="none" height="24" width="24"/></g><g><g/><path d="M12,5V1L7,6l5,5V7c3.31,0,6,2.69,6,6s-2.69,6-6,6s-6-2.69-6-6H4c0,4.42,3.58,8,8,8s8-3.58,8-8S16.42,5,12,5z"/></g></svg>`;
797
+ const pause = r`<svg height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><title>Pause</title><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`;
798
+ const play = r`<svg height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><title>Play</title><path d="M0 0h24v24H0V0z" fill="none"/><path d="M10 8.64L15.27 12 10 15.36V8.64M8 5v14l11-7L8 5z"/></svg>`;
799
+
800
+ class MyTimer extends HTMLElement {
801
+
802
+ constructor({duration}={}) {
803
+ super();
804
+ this.duration = getArg(this, 'duration', duration)*1;
805
+ this.end = null;
806
+ this.remaining = this.duration * 1000;
807
+ this.render();
808
+ }
809
+
810
+ render() {
811
+ const min = Math.floor(this.remaining / 60000);
812
+ const sec = pad(min, Math.floor((this.remaining / 1000) % 60));
813
+ const hun = pad(true, Math.floor((this.remaining % 1000) / 10));
814
+ r(this)`
815
+ <my-timer>
816
+ ${min ? `${min}:${sec}` : `${sec}.${hun}`}
817
+ <footer>
818
+ <style>
819
+ :host { display: inline-block; min-width: 90px; font-size: 30px; text-align: center; padding: 0.2em; margin: 0.2em 0.1em;
820
+ footer { user-select: none }
821
+ }
822
+ </style>
823
+ ${
824
+ this.remaining === 0
825
+ ? ''
826
+ : this.running
827
+ ? r`<span onclick=${this.pause}>${pause}</span>`
828
+ : r`<span onclick=${this.start}>${play}</span>`
829
+ }
830
+ <span onclick=${this.reset}>${replay}</span>
831
+ </footer>
832
+ </my-timer>`;
833
+ }
834
+
835
+ start() {
836
+ this.end = Date.now() + this.remaining;
837
+ this.tick();
838
+ }
839
+
840
+ pause() {
841
+ this.end = null;
842
+ this.render();
843
+ }
844
+
845
+ reset() {
846
+ this.remaining = this.duration * 1000;
847
+ this.end = this.running ? Date.now() + this.remaining : null;
848
+ this.render();
849
+ }
850
+
851
+ tick() {
852
+ if (this.running) {
853
+ this.remaining = Math.max(0, this.end - Date.now());
854
+ this.render();
855
+ requestAnimationFrame(() => this.tick());
856
+ }
857
+ }
858
+
859
+ get running() {
860
+ return this.end && this.remaining;
861
+ }
862
+ }
863
+ customElements.define('my-timer', MyTimer);
864
+
865
+ function pad(pad, val) {
866
+ return pad ? String(val).padStart(2, '0') : val;
867
+ }
868
+ </script>
869
+ <my-timer duration="7"></my-timer>
870
+ <my-timer duration="60"></my-timer>
871
+ <my-timer duration="300"></my-timer>
872
+ ```
873
+
874
+
875
+
876
+ ## Upcoming Features
877
+
878
+ These are possible features to come:
879
+
880
+ 1. Optional shadow DOM support
881
+ 2. Optional JSX support
882
+ 3. Option to render automatically when properties change, without calling render()
883
+ 4. Faster rendering performance.