solarite 0.1.0 → 0.1.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.
- package/build/build.bat +1 -0
- package/deno.lock +176 -0
- package/dist/Solarite-debug.js +456 -1118
- package/dist/Solarite.js +438 -1216
- package/dist/Solarite.min.js +2 -2
- package/docs/index.md +366 -216
- package/docs/js/codemirror/themeSolarIce.js +1 -1
- package/docs/js/documentation.js +1 -1
- package/docs/js/ui/FlexResizer.js +1 -2
- package/docs/media/documentation.css +2 -2
- package/index.html +40 -23
- package/package.json +1 -1
- package/readme.md +11 -1
- package/src/solarite/ExprPath.js +92 -3
- package/src/solarite/Globals.js +11 -0
- package/src/solarite/MultiValueMap.js +5 -1
- package/src/solarite/NodeGroup.js +54 -124
- package/src/solarite/NodeGroupManager.js +15 -125
- package/src/solarite/Shell.js +5 -5
- package/src/solarite/Solarite.js +5 -2
- package/src/solarite/Template.js +131 -8
- package/src/solarite/createSolarite.js +18 -6
- package/src/solarite/getArg.js +11 -11
- package/src/solarite/hash.js +1 -1
- package/src/solarite/r.js +24 -14
- package/src/solarite/watch.js +1 -1
- package/src/solarite/watch2.js +3 -2
- package/src/solarite/watch3.js +91 -0
- package/src/unused/onConnect.js +79 -0
- package/tests/Solarite.test.js +281 -34
- package/tests/Testimony.js +274 -67
- package/tests/index.html +3 -34
- package/tests/run.bat +2 -0
- package/tests/NodeGroup.test.js +0 -115
- package/tests/Shell.test.js +0 -75
package/docs/index.md
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Solarite
|
|
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="
|
|
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">
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
<!-- To
|
|
7
|
+
<!-- To convert documentation to html: (1) Open in Typora. (2) Select the GitHub theme. (3) Export as html with styles to index.html. -->
|
|
8
8
|
|
|
9
|
-
# Solarite
|
|
9
|
+
# Solarite
|
|
10
10
|
|
|
11
|
-
Solarite is a small (
|
|
11
|
+
Solarite is a small (8KB min+gzip), fast, compilation-free JavaScript library to enhance your vanilla web components. Features:
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- Very similar to writing native Web Components.
|
|
14
|
+
- Minimal DOM updates when rendering.
|
|
15
|
+
- No magic: Rendering only when you want it, via the manually invoked render() method.
|
|
16
|
+
- Local scoped styles: Inherit external styles but define new styles that apply only to the web component and its children.
|
|
17
|
+
- Elements with `id` or `data-id` attributes become class properties.
|
|
18
|
+
- Attributes are passed as constructor arguments to nested Solarite components.
|
|
19
|
+
- 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
|
+
- MIT license. Free for commercial use. No attribution needed.
|
|
14
21
|
|
|
15
|
-
|
|
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.
|
|
16
23
|
|
|
17
24
|
```javascript
|
|
18
|
-
|
|
19
|
-
import {Solarite, r} from '/src/solarite/Solarite.js';
|
|
25
|
+
import {r} from './dist/Solarite.js';
|
|
20
26
|
|
|
21
|
-
class ShoppingList extends
|
|
27
|
+
class ShoppingList extends HTMLElement {
|
|
22
28
|
constructor(items=[]) {
|
|
23
29
|
super();
|
|
24
30
|
this.items = items;
|
|
31
|
+
this.render();
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
addItem() {
|
|
@@ -34,182 +41,145 @@ class ShoppingList extends Solarite {
|
|
|
34
41
|
this.render();
|
|
35
42
|
}
|
|
36
43
|
|
|
37
|
-
render() {
|
|
38
|
-
this
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
render() {
|
|
45
|
+
// Think of r(this) as like:
|
|
46
|
+
// this.outerHTML = `<shopping-list>...`
|
|
47
|
+
// but rendering only minimal DOM updates when the html changes.
|
|
48
|
+
r(this)`
|
|
49
|
+
<shopping-list>
|
|
50
|
+
<style> /* scoped styles */
|
|
51
|
+
:host input { width: 80px }
|
|
52
|
+
</style>
|
|
53
|
+
|
|
54
|
+
<button onclick=${this.addItem}>Add Item</button>
|
|
55
|
+
|
|
56
|
+
${this.items.map(item => r`
|
|
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
|
+
}}>
|
|
68
|
+
<button onclick=${()=>this.removeItem(item)}>x</button>
|
|
69
|
+
</div>
|
|
70
|
+
`)}
|
|
71
|
+
|
|
72
|
+
<pre>items = ${JSON.stringify(this.items, null, 4)}</pre>
|
|
73
|
+
</shopping-list>`
|
|
53
74
|
}
|
|
54
75
|
}
|
|
55
|
-
document.body.append(new ShoppingList()); // adds a child named <shopping-list>
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
==TODO Does it re-render all Items on update?==
|
|
59
|
-
|
|
60
|
-
## Features
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- Creates native HTML Elements and Web Components which can be used anywhere in your document and alongside other libraries.
|
|
66
|
-
- No need to set up state. Instead, use any regular variables or data structures in html templates.
|
|
67
|
-
- Minimal updates on render
|
|
68
|
-
- Local (scoped) styles
|
|
69
|
-
- Two-way form element binding.
|
|
70
|
-
- Optional shadow DOM (coming soon)
|
|
71
|
-
- Optional JSX support (coming soon)
|
|
72
|
-
- MIT license. Free for commercial use. No attribution needed.
|
|
77
|
+
customElements.define('shopping-list', ShoppingList);
|
|
78
|
+
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.
|
|
75
82
|
|
|
76
|
-
|
|
83
|
+
To use, import one of these pre-bundled es6 modules into your project:
|
|
77
84
|
|
|
78
|
-
- [
|
|
79
|
-
- [
|
|
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
|
|
80
87
|
|
|
81
|
-
|
|
88
|
+
Or get Solarite from GitHub or NPM:
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
- [Solarite GitHub Repository](https://github.com/Vorticode/solarite)
|
|
91
|
+
- `git clone https://github.com/Vorticode/solarite.git`
|
|
92
|
+
- `npm install solarite`
|
|
84
93
|
|
|
85
94
|
## Concepts
|
|
86
95
|
|
|
87
|
-
###
|
|
96
|
+
### Web Components
|
|
88
97
|
|
|
89
|
-
In this minimal example, we make a new class called `MyComponent`
|
|
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.
|
|
90
99
|
|
|
91
|
-
All browsers require custom web component names to have
|
|
100
|
+
All browsers require custom web component tag names to have at least one dash in the middle.
|
|
92
101
|
|
|
93
102
|
```javascript
|
|
94
|
-
import {
|
|
103
|
+
import {r} from './dist/Solarite.js';
|
|
95
104
|
|
|
96
|
-
class MyComponent extends
|
|
97
|
-
name = '
|
|
105
|
+
class MyComponent extends HTMLElement {
|
|
106
|
+
name = 'Solarite';
|
|
107
|
+
|
|
108
|
+
constructor() {
|
|
109
|
+
super();
|
|
110
|
+
this.render();
|
|
111
|
+
}
|
|
112
|
+
|
|
98
113
|
render() {
|
|
99
|
-
this
|
|
114
|
+
r(this)`<my-component>Hello <b>${this.name}!<b></my-component>`
|
|
100
115
|
}
|
|
101
116
|
}
|
|
102
117
|
|
|
118
|
+
customElements.define('my-component', MyComponent);
|
|
103
119
|
document.body.append(new MyComponent());
|
|
104
120
|
```
|
|
105
121
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Note that the template strings use the `r` prefix. This `r` parses the html into a data structure that Red Component can use.
|
|
109
|
-
|
|
110
|
-
Alternatively, instead of instantiating the element in JavaScript, we could can instantiate the element directly from html. This only works if we first call `MyComponent.define()` so so that our element's tag name is mapped to our class:
|
|
111
|
-
|
|
112
|
-
```html2
|
|
113
|
-
<script>
|
|
114
|
-
// ...
|
|
115
|
-
// document.body.append(new MyComponent());
|
|
116
|
-
MyComponent.define()
|
|
117
|
-
</script>
|
|
122
|
+
Alternatively, instead of instantiating the element in JavaScript, we could can instantiate the element directly from html:
|
|
118
123
|
|
|
124
|
+
```html
|
|
119
125
|
<my-component></my-component>
|
|
120
126
|
```
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
If you want the component to have a tag name that's different than the name derived from the class name, you can pass a different name to `define()`:
|
|
125
|
-
|
|
126
|
-
```javascript2
|
|
127
|
-
MyComponent.define('my-awesome-component')
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### The render() function, r, and this.html
|
|
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!
|
|
131
129
|
|
|
132
|
-
|
|
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.
|
|
133
131
|
|
|
134
|
-
|
|
132
|
+
### render() and r()
|
|
135
133
|
|
|
136
|
-
|
|
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.
|
|
137
135
|
|
|
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.
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
Wrapping the web component's html in its tag name is optional. You could instead just assign the html for the child elements to `this.html`. But then you will have to set any attributes on your web component some other way.
|
|
138
|
+
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:
|
|
141
139
|
|
|
142
140
|
```javascript
|
|
143
|
-
import {
|
|
141
|
+
import {r} from './dist/Solarite.js';
|
|
144
142
|
|
|
145
|
-
class MyComponent extends
|
|
143
|
+
class MyComponent extends HTMLElement {
|
|
146
144
|
name = 'Solarite';
|
|
147
145
|
render() {
|
|
148
146
|
// With optional element tags:
|
|
149
|
-
// this
|
|
147
|
+
// r(this)`<my-component class="big">Hello <b>${this.name}!<b></my-component>`
|
|
150
148
|
|
|
151
149
|
// Without optional element tags:
|
|
152
|
-
this
|
|
150
|
+
r(this)`Hello <b>${this.name}!<b>`;
|
|
151
|
+
this.setAttribute('class', 'big');
|
|
153
152
|
}
|
|
154
153
|
}
|
|
155
|
-
|
|
154
|
+
customElements.define('my-component', MyComponent);
|
|
156
155
|
document.body.append(new MyComponent());
|
|
157
156
|
```
|
|
158
157
|
|
|
159
|
-
If you do
|
|
160
|
-
|
|
161
|
-
### Inheriting from existing DOM elements.
|
|
162
|
-
|
|
163
|
-
Suppose you want to use a custom component for each `<tr>` in a `<table>`. Html won't allow you to put just any element as a child of table or tbody. In this case you can make your web component inherit from the browser's built in `<tr>` element:
|
|
164
|
-
|
|
165
|
-
```javascript
|
|
166
|
-
import {Solarite, r} from '../dist/Solarite.js';
|
|
167
|
-
|
|
168
|
-
class LineItem extends Solarite('tr') {
|
|
169
|
-
constructor(user) {
|
|
170
|
-
super();
|
|
171
|
-
this.user = user;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
render() {
|
|
175
|
-
this.html = r`
|
|
176
|
-
<tr>
|
|
177
|
-
<td>${this.user.name}</td>
|
|
178
|
-
<td>${this.user.email}</td>
|
|
179
|
-
</tr>`
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
LineItem.define();
|
|
183
|
-
|
|
184
|
-
let table = document.createElement('table')
|
|
185
|
-
for (let i=0; i<10; i++) {
|
|
186
|
-
let user = {name: 'User ' + i, email: 'user'+i+'@example.com'};
|
|
187
|
-
table.append(new LineItem(user));
|
|
188
|
-
}
|
|
189
|
-
document.body.append(table)
|
|
190
|
-
```
|
|
158
|
+
If you do wrap the components html in its tag, that tag name must exactly match the tag name passed to customElements.define().
|
|
191
159
|
|
|
192
160
|
### Loops
|
|
193
161
|
|
|
194
|
-
|
|
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:
|
|
195
163
|
|
|
196
164
|
```javascript
|
|
197
|
-
import {
|
|
165
|
+
import {r} from './dist/Solarite.js';
|
|
198
166
|
|
|
199
|
-
class TodoList extends
|
|
167
|
+
class TodoList extends HTMLElement {
|
|
200
168
|
render() {
|
|
201
|
-
this
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
169
|
+
r(this)`
|
|
170
|
+
<todo-list>
|
|
171
|
+
${this.items.map(item =>
|
|
172
|
+
r`${item}<br>`
|
|
173
|
+
)}
|
|
174
|
+
</todo-list>`
|
|
207
175
|
}
|
|
208
176
|
}
|
|
177
|
+
customElements.define('todo-list', TodoList);
|
|
209
178
|
|
|
210
179
|
let list = new TodoList();
|
|
211
180
|
list.items = ['one', 'two', 'three'];
|
|
212
|
-
|
|
181
|
+
list.render();
|
|
182
|
+
document.body.append(list);
|
|
213
183
|
|
|
214
184
|
list.items[1] = '2';
|
|
215
185
|
list.render();
|
|
@@ -218,37 +188,40 @@ list.items.splice(1, 0, 'two and a half');
|
|
|
218
188
|
list.render();
|
|
219
189
|
```
|
|
220
190
|
|
|
221
|
-
Note that nested template literals must also have the `r` prefix. Otherwise they'll be rendered as escaped text instead of HTML elements.
|
|
222
|
-
|
|
223
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.
|
|
224
192
|
|
|
193
|
+
Note that nested template literals must also have the `r` prefix. Otherwise they'll be rendered as escaped text instead of HTML elements.
|
|
194
|
+
|
|
225
195
|
### Attributes
|
|
226
196
|
|
|
227
|
-
|
|
197
|
+
Dynamic attributes can be specified by inserting expressions inside a tag. An expression can be part or all of an attribute value, or a string specifying multiple whole attributes. For example:
|
|
228
198
|
|
|
229
199
|
```javascript
|
|
230
|
-
import {
|
|
200
|
+
import {r} from './dist/Solarite.js';
|
|
231
201
|
|
|
232
202
|
let style = 'width: 100px; height: 40px; background: orange';
|
|
233
203
|
let isEditable = true;
|
|
234
204
|
let height = 40;
|
|
235
205
|
|
|
236
|
-
class AttributeDemo extends
|
|
206
|
+
class AttributeDemo extends HTMLElement {
|
|
237
207
|
render() {
|
|
238
|
-
this
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
208
|
+
r(this)`
|
|
209
|
+
<attribute-demo class="big">
|
|
210
|
+
|
|
211
|
+
<div style=${style}>Look at me</div>
|
|
212
|
+
|
|
213
|
+
<div style="${'width: 100px'}; height: ${height}px; background: gray">Look at me</div>
|
|
242
214
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
<div style="width: 100px; height: 40px; background: red" ${'title="I have a title"'}>Hover me</div>
|
|
215
|
+
<div style="width: 100px; height: 40px; background: red" ${'title="I have a title"'}>Hover me</div>
|
|
246
216
|
|
|
247
|
-
|
|
248
|
-
|
|
217
|
+
<div style="width: 100px; height: 40px; background: brown" contenteditable=${isEditable} >Edit me</div>
|
|
218
|
+
</attribute-demo>`
|
|
249
219
|
}
|
|
250
220
|
}
|
|
251
|
-
|
|
221
|
+
customElements.define('attribute-demo', AttributeDemo);
|
|
222
|
+
let ad = new AttributeDemo();
|
|
223
|
+
ad.render();
|
|
224
|
+
document.body.append(ad);
|
|
252
225
|
```
|
|
253
226
|
|
|
254
227
|
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.
|
|
@@ -260,92 +233,110 @@ Note that attributes can also be assigned to the root element, such as `class="b
|
|
|
260
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.
|
|
261
234
|
|
|
262
235
|
```javascript
|
|
263
|
-
import {
|
|
236
|
+
import {r} from './dist/Solarite.js';
|
|
264
237
|
|
|
265
|
-
class EventDemo extends
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
238
|
+
class EventDemo extends HTMLElement {
|
|
239
|
+
constructor() {
|
|
240
|
+
super();
|
|
241
|
+
this.render();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
showMessage(message) {
|
|
245
|
+
alert(message);
|
|
246
|
+
}
|
|
247
|
+
|
|
270
248
|
render() {
|
|
271
|
-
this
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
249
|
+
r(this)`
|
|
250
|
+
<event-demo>
|
|
251
|
+
<button onclick=${(ev, el)=>alert('Element ' + el.tagName + ' clicked!')}>Click me</button>
|
|
252
|
+
<button onclick=${[this.showMessage, 'I too was clicked!']}>Click me too!</button>
|
|
253
|
+
</event-demo>`
|
|
276
254
|
}
|
|
277
|
-
}
|
|
255
|
+
}
|
|
256
|
+
customElements.define('event-demo', EventDemo);
|
|
278
257
|
document.body.append(new EventDemo());
|
|
279
258
|
```
|
|
280
259
|
|
|
281
|
-
Event binding with an array containing a function and its arguments is slightly faster, since when render() is called,
|
|
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.
|
|
282
261
|
|
|
283
262
|
Make sure to put your events inside `${...}` expressions, because classic events can't reference variables in the current scope.
|
|
284
263
|
|
|
285
264
|
### Two-Way Binding
|
|
286
265
|
|
|
287
|
-
==TODO: This demo should use auto-rendering==
|
|
288
|
-
|
|
289
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:
|
|
290
267
|
|
|
291
268
|
```javascript
|
|
292
|
-
import {
|
|
269
|
+
import {r} from './dist/Solarite.js';
|
|
293
270
|
|
|
294
|
-
class BindingDemo extends
|
|
271
|
+
class BindingDemo extends HTMLElement {
|
|
295
272
|
|
|
296
273
|
constructor() {
|
|
297
|
-
|
|
274
|
+
super();
|
|
298
275
|
this.count = 0;
|
|
299
|
-
|
|
276
|
+
this.render();
|
|
300
277
|
}
|
|
301
278
|
|
|
302
279
|
render() {
|
|
303
|
-
this
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
280
|
+
r(this)`
|
|
281
|
+
<binding-demo>
|
|
282
|
+
<input type="number" value=${this.count}
|
|
283
|
+
oninput=${ev => {
|
|
284
|
+
this.count = ev.target.value;
|
|
285
|
+
this.render();
|
|
286
|
+
}}>
|
|
287
|
+
<pre>count is ${this.count}</pre>
|
|
288
|
+
<button onclick=${()=> {
|
|
289
|
+
this.count = 0;
|
|
290
|
+
this.render();
|
|
291
|
+
}}>Reset</button>
|
|
292
|
+
</binding-demo>`
|
|
309
293
|
}
|
|
310
294
|
}
|
|
295
|
+
customElements.define('binding-demo', BindingDemo);
|
|
296
|
+
|
|
311
297
|
document.body.append(new BindingDemo());
|
|
312
298
|
```
|
|
313
299
|
|
|
314
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.
|
|
315
301
|
|
|
316
|
-
###
|
|
302
|
+
### Id's
|
|
317
303
|
|
|
318
|
-
Any element in the html with an `id` or `data-id` attribute is automatically bound to a property with the same name on the
|
|
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:
|
|
319
305
|
|
|
320
306
|
```javascript
|
|
321
|
-
import {
|
|
307
|
+
import {r} from './dist/Solarite.js';
|
|
322
308
|
|
|
323
|
-
class RaceTeam extends
|
|
309
|
+
class RaceTeam extends HTMLElement {
|
|
324
310
|
constructor() {
|
|
325
311
|
super();
|
|
326
312
|
|
|
327
313
|
// Id's are not set until render() is first called.
|
|
328
314
|
this.render();
|
|
329
315
|
|
|
316
|
+
// Change the value of the input.
|
|
330
317
|
// No need to render() again since we're changing the DOM manually.
|
|
331
|
-
this.driver.value = '
|
|
318
|
+
this.driver.value = 'Luigi';
|
|
332
319
|
}
|
|
333
320
|
|
|
334
|
-
render() {
|
|
321
|
+
render() {
|
|
322
|
+
r(this)`
|
|
335
323
|
<race-team>
|
|
336
|
-
<input id="driver" value="
|
|
324
|
+
<input id="driver" value="Mario">
|
|
337
325
|
<div data-id="car">Cutlas Supreme</div>
|
|
338
326
|
<div data-id="instructor.name">Lightning McQueen</div>
|
|
339
327
|
</race-team>`
|
|
340
328
|
}
|
|
341
329
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
330
|
+
customElements.define('race-team', RaceTeam);
|
|
331
|
+
let raceTeam = new RaceTeam();
|
|
332
|
+
document.body.append(raceTeam);
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
raceTeam.car.style.border = '1px solid green';
|
|
345
336
|
|
|
346
337
|
```
|
|
347
338
|
|
|
348
|
-
|
|
339
|
+
Id's that have values matching built-in HTMLElement attribute names such as `title` or `disabled` are not allowed.
|
|
349
340
|
|
|
350
341
|
### Scoped Styles
|
|
351
342
|
|
|
@@ -353,71 +344,230 @@ Html with `style` elements will be rewritten so that the `:host` selector applie
|
|
|
353
344
|
|
|
354
345
|
These local, "scoped" styles are implemented by:
|
|
355
346
|
|
|
356
|
-
1. Adding a `data-style` attribute to the root element with a unique, incrementing id value.
|
|
347
|
+
1. Adding a `data-style` attribute to the root element with a unique, incrementing id value for each instance.
|
|
357
348
|
2. Replacing any `:host` selectors inside the style with `element-name[data-style="1"]`. For example the `:host` selector below becomes `fancy-text[data-style="1"]`.
|
|
358
349
|
|
|
359
350
|
```javascript
|
|
360
|
-
import {
|
|
351
|
+
import {r} from './dist/Solarite.js';
|
|
361
352
|
|
|
362
|
-
class FancyText extends
|
|
353
|
+
class FancyText extends HTMLElement {
|
|
363
354
|
render() {
|
|
364
|
-
|
|
355
|
+
r(this)`
|
|
365
356
|
<fancy-text>
|
|
366
357
|
<style>
|
|
367
|
-
:host { border: 10px dashed red }
|
|
368
|
-
:host p { text-shadow: 0 0
|
|
358
|
+
:host { display: block; border: 10px dashed red }
|
|
359
|
+
:host p { text-shadow: 0 0 3px #f40 }
|
|
369
360
|
</style>
|
|
370
361
|
<p>I have a red border and shadow!</p>
|
|
371
362
|
</fancy-text>`
|
|
363
|
+
|
|
364
|
+
/* This is rewritten as:
|
|
365
|
+
<fancy-text data-style="1">
|
|
366
|
+
<style>
|
|
367
|
+
fancy-text[data-style="1"] { display: block; border: 10px dashed red }
|
|
368
|
+
fancy-text[data-style="1"] p { text-shadow: 0 0 3px #f40 }
|
|
369
|
+
</style>
|
|
370
|
+
<p>I have a red border and shadow!</p>
|
|
371
|
+
</fancy-text>`
|
|
372
|
+
*/
|
|
372
373
|
}
|
|
373
374
|
}
|
|
374
|
-
|
|
375
|
+
customElements.define('fancy-text', FancyText);
|
|
376
|
+
let el = new FancyText();
|
|
377
|
+
el.render();
|
|
378
|
+
document.body.append(el);
|
|
375
379
|
```
|
|
376
380
|
|
|
377
|
-
Note that if
|
|
381
|
+
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.
|
|
378
382
|
|
|
379
383
|
### Sub Components
|
|
380
384
|
|
|
381
|
-
|
|
385
|
+
When one Solarite component is embedded within another, its attributes and children are passed as arguments to the constructor:
|
|
386
|
+
|
|
387
|
+
```javascript
|
|
388
|
+
import {r} from './dist/Solarite.js';
|
|
382
389
|
|
|
383
|
-
|
|
390
|
+
class NotesItem extends HTMLElement {
|
|
391
|
+
// Constructor receives item object from attributes.
|
|
392
|
+
constructor({item}, children) {
|
|
393
|
+
super();
|
|
394
|
+
this.item = item;
|
|
395
|
+
this.render();
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
render() {
|
|
399
|
+
r(this)`
|
|
400
|
+
<notes-item>
|
|
401
|
+
<b>${this.item.name}</b> - ${this.item.description}<br>
|
|
402
|
+
</notes-item>`
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
customElements.define('notes-item', NotesItem);
|
|
384
406
|
|
|
385
|
-
|
|
407
|
+
class NotesList extends HTMLElement {
|
|
408
|
+
render() {
|
|
409
|
+
r(this)`
|
|
410
|
+
<notes-list>
|
|
411
|
+
${this.items.map(item => // Pass item object to NotesItem constructor:
|
|
412
|
+
r`<notes-item item=${item}"></notes-item>`
|
|
413
|
+
)}
|
|
414
|
+
</notes-list>`
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
customElements.define('notes-list', NotesList);
|
|
418
|
+
|
|
419
|
+
let list = new NotesList();
|
|
420
|
+
list.items = [
|
|
421
|
+
{
|
|
422
|
+
name: 'English',
|
|
423
|
+
description: 'See spot run.'
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
{
|
|
427
|
+
name: 'Science',
|
|
428
|
+
description: 'Snails are mollusks.'
|
|
429
|
+
}
|
|
430
|
+
]
|
|
431
|
+
list.render();
|
|
432
|
+
document.body.append(list);
|
|
433
|
+
```
|
|
386
434
|
|
|
387
|
-
|
|
435
|
+
Note that calling `render()` on a parent component will call it on sub-components too.
|
|
388
436
|
|
|
389
437
|
### Classless Elements
|
|
390
438
|
|
|
391
|
-
|
|
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.
|
|
392
440
|
|
|
393
|
-
|
|
441
|
+
```javascript
|
|
442
|
+
import {r} from './dist/Solarite.js';
|
|
443
|
+
|
|
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
|
+
);
|
|
461
|
+
document.body.append(button);
|
|
462
|
+
```
|
|
394
463
|
|
|
395
|
-
|
|
464
|
+
### Extending Other DOM Elements.
|
|
465
|
+
|
|
466
|
+
Suppose you want to use a custom component for each `<tr>` in a `<table>`. Html won't allow you to put just any element as a child of table or tbody. In this case you can make your web component inherit from the browser's built in `<tr>` element, by passing it as the third argument to `customElements.define`:
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
import {r} from './dist/Solarite.js';
|
|
470
|
+
|
|
471
|
+
class LineItem extends HTMLElement {
|
|
472
|
+
constructor(user) {
|
|
473
|
+
super();
|
|
474
|
+
this.user = user;
|
|
475
|
+
this.render();
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
render() {
|
|
479
|
+
r(this)`
|
|
480
|
+
<th>${this.user.name}</td>
|
|
481
|
+
<td>${this.user.email}</td>`
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
customElements.define('line-item', LineItem, HTMLTableRowElement);
|
|
486
|
+
|
|
487
|
+
let table = document.createElement('table')
|
|
488
|
+
for (let i=0; i<10; i++) {
|
|
489
|
+
let user = {name: 'User ' + i, email: 'user'+i+'@example.com'};
|
|
490
|
+
table.append(new LineItem(user));
|
|
491
|
+
}
|
|
492
|
+
document.body.append(table);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### The Solarite Class (Experimental)
|
|
496
|
+
|
|
497
|
+
Instead of inheriting from HTMLElement, you can inherit from the `Solarite` class, which will add a little bit of magic to your web component:
|
|
498
|
+
|
|
499
|
+
1. `render()` is automatically called when the element is added to the DOM, via a `connectedCallback()` function in the Solarite parent class.
|
|
500
|
+
2. `customElements.define()` is automatically called when an element is instantiated via `new`. It defines the element name based on the class name, by converting the class name to a tag name with dashes, because browsers require all custom elements to have at least one dash within the name. If it can't find at least one place to put a dash, it will append `-element` to the end.
|
|
501
|
+
|
|
502
|
+
If you want the component to have a tag name that's different than the name derived from the class name, you can pass a different name to `define()`:
|
|
503
|
+
|
|
504
|
+
```javascript
|
|
505
|
+
import {r, Solarite} from './dist/Solarite.js';
|
|
506
|
+
|
|
507
|
+
class TodoList extends Solarite {
|
|
508
|
+
render() {
|
|
509
|
+
r(this)`
|
|
510
|
+
<todo-list>
|
|
511
|
+
${this.items.map(item =>
|
|
512
|
+
r`${item}<br>`
|
|
513
|
+
)}
|
|
514
|
+
</todo-list>`
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// This is called automatically when we extend from Solarite:
|
|
518
|
+
//customElements.define('todo-list', TodoList);
|
|
396
519
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
type="checkbox"
|
|
405
|
-
checked={task.completed}
|
|
406
|
-
onChange={() => toggleTask(index)}
|
|
407
|
-
/>
|
|
408
|
-
{task.text}
|
|
409
|
-
<button onClick={() => deleteTask(index)}>Delete</button>
|
|
410
|
-
</li>
|
|
411
|
-
))}
|
|
412
|
-
</ul>
|
|
520
|
+
let list = new TodoList();
|
|
521
|
+
list.items = ['one', 'two', 'three'];
|
|
522
|
+
//list.render(); render() is called automatically when appended to body.
|
|
523
|
+
document.body.append(list);
|
|
524
|
+
|
|
525
|
+
list.items[1] = '2';
|
|
526
|
+
list.render();
|
|
413
527
|
```
|
|
414
528
|
|
|
415
|
-
|
|
529
|
+
## How it works
|
|
530
|
+
|
|
531
|
+
Suppose you're looping over an array of 10 objects and printing them to a list or table:
|
|
532
|
+
|
|
533
|
+
```javascript
|
|
534
|
+
import {r} from './dist/Solarite.js';
|
|
416
535
|
|
|
417
|
-
|
|
536
|
+
class MyTasks extends HTMLElement {
|
|
537
|
+
tasks = [];
|
|
538
|
+
|
|
539
|
+
deleteTask(index) {
|
|
540
|
+
this.tasks.splice(index, 1);
|
|
541
|
+
this.render();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
render() {
|
|
545
|
+
r(this)`
|
|
546
|
+
<div>
|
|
547
|
+
${this.tasks.map((task, index) => r`
|
|
548
|
+
<div>
|
|
549
|
+
${task.text}
|
|
550
|
+
<button onClick=${() => this.deleteTask(index)}>Delete</button>
|
|
551
|
+
</div>`
|
|
552
|
+
)}
|
|
553
|
+
</div>`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
customElements.define('my-tasks', MyTasks);
|
|
557
|
+
|
|
558
|
+
let myTasks = new MyTasks();
|
|
559
|
+
for (let i=0; i<10; i++)
|
|
560
|
+
myTasks.tasks.push({
|
|
561
|
+
text: 'Item ' + i,
|
|
562
|
+
completed: false
|
|
563
|
+
});
|
|
564
|
+
myTasks.render();
|
|
565
|
+
document.body.append(myTasks);
|
|
566
|
+
```
|
|
418
567
|
|
|
419
|
-
|
|
568
|
+
When `render()` is called:
|
|
420
569
|
|
|
421
|
-
|
|
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.
|
|
572
|
+
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.
|
|
422
573
|
|
|
423
|
-
### Solid.js
|