hyper-element 0.11.1 → 0.12.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/README.md +789 -395
- package/index.d.ts +188 -0
- package/package.json +40 -8
- package/example/index.ejs +0 -28
- package/example/index.js +0 -2
- package/example/logic.js +0 -95
- package/example/package.json +0 -15
- package/example/poi.config.js +0 -4
- package/example/test-elem.js +0 -118
- package/source/bundle.js +0 -562
- package/source/hyperElement.js +0 -483
- package/test.html +0 -322
package/README.md
CHANGED
|
@@ -1,639 +1,1033 @@
|
|
|
1
1
|
# hyper-element
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/hyper-element)
|
|
4
|
+
[](https://bundlephobia.com/package/hyper-element)
|
|
5
|
+
[](https://github.com/codemeasandwich/hyper-element/actions/workflows/publish.yml)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://github.com/codemeasandwich/hyper-element)
|
|
8
|
+
[](https://caniuse.com/es6)
|
|
4
9
|
|
|
5
|
-
[
|
|
6
|
-
[](https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js)
|
|
7
|
-
[](https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js)
|
|
10
|
+
Combining the best of [hyperHTML] and [Custom Elements]! Your new custom-element will be rendered with the super fast **hyperHTML** and will react to tag attribute and store changes.
|
|
8
11
|
|
|
12
|
+
### If you like it, please [★ it on github](https://github.com/codemeasandwich/hyper-element)
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## why hyper-element
|
|
14
|
+
# Installation
|
|
13
15
|
|
|
16
|
+
## npm
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* Simple yet powerful [Api](#api)
|
|
19
|
-
* Built in [template](#templates) system to customise the rendered output
|
|
20
|
-
* Inline style objects supported (similar to React)
|
|
21
|
-
* First class support for [data stores](#example-of-connecting-to-a-data-store)
|
|
22
|
-
* Pass `function` to other custom hyper-elements via there tag attribute
|
|
18
|
+
```bash
|
|
19
|
+
npm install hyper-element
|
|
20
|
+
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
### ES6 Modules
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
```js
|
|
25
|
+
import hyperElement from 'hyper-element';
|
|
27
26
|
|
|
28
|
-
|
|
27
|
+
customElements.define(
|
|
28
|
+
'my-elem',
|
|
29
|
+
class extends hyperElement {
|
|
30
|
+
render(Html) {
|
|
31
|
+
Html`Hello ${this.attrs.who}!`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
```
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
- [Api](#api)
|
|
32
|
-
* [Define your element](#define-your-element)
|
|
33
|
-
+ [render](#render)
|
|
34
|
-
+ [Html](#html)
|
|
35
|
-
+ [Html.wire](#htmlwire)
|
|
36
|
-
+ [Html.lite](#htmllite)
|
|
37
|
-
+ [setup](#setup)
|
|
38
|
-
+ [this](#this)
|
|
39
|
-
* [Advanced attributes](#advanced-attributes)
|
|
40
|
-
* [Templates](#templates)
|
|
41
|
-
* [Fragments](#fragments)
|
|
42
|
-
+ [fragment templates](#fragment-templates)
|
|
43
|
-
+ [Async fragment templates](#asynchronous-fragment-templates)
|
|
44
|
-
* [Styling](#styling)
|
|
45
|
-
- [Connecting to a data store](#example-of-connecting-to-a-data-store)
|
|
46
|
-
* [backbone](#backbone)
|
|
47
|
-
* [mobx](#mobx)
|
|
48
|
-
* [redux](#redux)
|
|
37
|
+
### CommonJS
|
|
49
38
|
|
|
50
|
-
|
|
39
|
+
```js
|
|
40
|
+
const hyperElement = require('hyper-element');
|
|
51
41
|
|
|
52
|
-
|
|
42
|
+
customElements.define(
|
|
43
|
+
'my-elem',
|
|
44
|
+
class extends hyperElement {
|
|
45
|
+
render(Html) {
|
|
46
|
+
Html`Hello ${this.attrs.who}!`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
```
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
document.registerElement("my-elem", class extends hyperElement{
|
|
52
|
+
## CDN (Browser)
|
|
56
53
|
|
|
57
|
-
|
|
58
|
-
Html`hello ${this.attrs.who}`
|
|
59
|
-
}// END render
|
|
54
|
+
For browser environments without a bundler, include both hyperHTML and hyper-element via CDN:
|
|
60
55
|
|
|
61
|
-
|
|
56
|
+
```html
|
|
57
|
+
<script src="https://cdn.jsdelivr.net/npm/hyperhtml@latest/index.js"></script>
|
|
58
|
+
<script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/build/hyperElement.min.js"></script>
|
|
62
59
|
```
|
|
63
60
|
|
|
64
|
-
|
|
61
|
+
The `hyperElement` class will be available globally on `window.hyperElement`.
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
63
|
+
## Browser Support
|
|
64
|
+
|
|
65
|
+
hyper-element requires native ES6 class support and the Custom Elements v1 API:
|
|
66
|
+
|
|
67
|
+
| Browser | Version |
|
|
68
|
+
| ------- | ------- |
|
|
69
|
+
| Chrome | 67+ |
|
|
70
|
+
| Firefox | 63+ |
|
|
71
|
+
| Safari | 10.1+ |
|
|
72
|
+
| Edge | 79+ |
|
|
73
|
+
|
|
74
|
+
For older browsers, a [Custom Elements polyfill](https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements) may be required.
|
|
69
75
|
|
|
70
|
-
|
|
76
|
+
## Why hyper-element
|
|
77
|
+
|
|
78
|
+
- hyper-element is fast & small
|
|
79
|
+
- With only 1 dependency: [hyperHTML]
|
|
80
|
+
- With a completely stateless approach, setting and reseting the view is trivial
|
|
81
|
+
- Simple yet powerful [Interface](#interface)
|
|
82
|
+
- Built in [template](#templates) system to customise the rendered output
|
|
83
|
+
- Inline style objects supported (similar to React)
|
|
84
|
+
- First class support for [data stores](#connecting-to-a-data-store)
|
|
85
|
+
- Pass `function` to other custom hyper-elements via there tag attribute
|
|
86
|
+
|
|
87
|
+
# [Live Demo](https://jsfiddle.net/codemeasandwich/k25e6ufv/)
|
|
88
|
+
|
|
89
|
+
## Live Examples
|
|
90
|
+
|
|
91
|
+
| Example | Description | Link |
|
|
92
|
+
| -------------------- | ----------------------------------- | ---------------------------------------------------------- |
|
|
93
|
+
| Hello World | Basic element creation | [CodePen](https://codepen.io/codemeasandwich/pen/VOQpqz) |
|
|
94
|
+
| Attach a Store | Store integration with setup() | [CodePen](https://codepen.io/codemeasandwich/pen/VOQWeN) |
|
|
95
|
+
| Templates | Using the template system | [CodePen](https://codepen.io/codemeasandwich/pen/LoQLrK) |
|
|
96
|
+
| Child Element Events | Passing functions to child elements | [CodePen](https://codepen.io/codemeasandwich/pen/rgdvPX) |
|
|
97
|
+
| Async Fragments | Loading content asynchronously | [CodePen](https://codepen.io/codemeasandwich/pen/MdQrVd) |
|
|
98
|
+
| Styling | React-style inline styles | [CodePen](https://codepen.io/codemeasandwich/pen/RmQVKY) |
|
|
99
|
+
| Full Demo | Complete feature demonstration | [JSFiddle](https://jsfiddle.net/codemeasandwich/k25e6ufv/) |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
- [Browser Support](#browser-support)
|
|
104
|
+
- [Define a Custom Element](#define-a-custom-element)
|
|
105
|
+
- [Lifecycle](#lifecycle)
|
|
106
|
+
- [Interface](#interface)
|
|
107
|
+
- [render](#render)
|
|
108
|
+
- [Html](#html)
|
|
109
|
+
- [Html.wire](#htmlwire)
|
|
110
|
+
- [Html.lite](#htmllite)
|
|
111
|
+
- [setup](#setup)
|
|
112
|
+
- [this](#this)
|
|
113
|
+
- [Advanced Attributes](#advanced-attributes)
|
|
114
|
+
- [Templates](#templates)
|
|
115
|
+
- [Basic Syntax](#basic-template-syntax)
|
|
116
|
+
- [Conditionals](#conditionals-if)
|
|
117
|
+
- [Negation](#negation-unless)
|
|
118
|
+
- [Iteration](#iteration-each)
|
|
119
|
+
- [Fragments](#fragments)
|
|
120
|
+
- [Styling](#styling)
|
|
121
|
+
- [Connecting to a Data Store](#connecting-to-a-data-store)
|
|
122
|
+
- [Backbone](#backbone)
|
|
123
|
+
- [MobX](#mobx)
|
|
124
|
+
- [Redux](#redux)
|
|
125
|
+
- [Best Practices](#best-practices)
|
|
126
|
+
- [Development](#development)
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
# Define a custom-element
|
|
71
131
|
|
|
72
132
|
```html
|
|
73
133
|
<!DOCTYPE html>
|
|
74
134
|
<html>
|
|
75
|
-
<head>
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
135
|
+
<head>
|
|
136
|
+
<script src="https://cdn.jsdelivr.net/npm/hyperhtml@latest/index.js"></script>
|
|
137
|
+
<script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/build/hyperElement.min.js"></script>
|
|
138
|
+
</head>
|
|
139
|
+
<body>
|
|
140
|
+
<my-elem who="world"></my-elem>
|
|
141
|
+
<script>
|
|
142
|
+
customElements.define(
|
|
143
|
+
'my-elem',
|
|
144
|
+
class extends hyperElement {
|
|
145
|
+
render(Html) {
|
|
146
|
+
Html`hello ${this.attrs.who}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
</script>
|
|
151
|
+
</body>
|
|
152
|
+
</html>
|
|
84
153
|
```
|
|
85
154
|
|
|
86
155
|
Output
|
|
87
156
|
|
|
88
157
|
```html
|
|
89
|
-
<my-elem who="world">
|
|
90
|
-
hello world
|
|
91
|
-
</my-elem>
|
|
158
|
+
<my-elem who="world"> hello world </my-elem>
|
|
92
159
|
```
|
|
93
160
|
|
|
94
|
-
|
|
161
|
+
**Live Example of [helloworld](https://codepen.io/codemeasandwich/pen/VOQpqz)**
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
# Lifecycle
|
|
166
|
+
|
|
167
|
+
When a hyper-element is connected to the DOM, it goes through the following initialization sequence:
|
|
168
|
+
|
|
169
|
+
1. Element connected to DOM
|
|
170
|
+
2. Unique identifier created
|
|
171
|
+
3. MutationObserver attached (watches for attribute/content changes)
|
|
172
|
+
4. Fragment methods defined (methods starting with capital letters)
|
|
173
|
+
5. Attributes and dataset attached to `this`
|
|
174
|
+
6. `setup()` called (if defined)
|
|
175
|
+
7. Initial `render()` called
|
|
176
|
+
|
|
177
|
+
After initialization, the element will automatically re-render when:
|
|
178
|
+
|
|
179
|
+
- Attributes change
|
|
180
|
+
- Content mutations occur (innerHTML/textContent changes)
|
|
181
|
+
- Store updates trigger `onStoreChange()`
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
# Interface
|
|
95
186
|
|
|
96
187
|
## Define your element
|
|
97
188
|
|
|
98
|
-
There are 2 functions. `render` is
|
|
189
|
+
There are 2 functions. `render` is _required_ and `setup` is _optional_
|
|
99
190
|
|
|
100
|
-
|
|
191
|
+
## render
|
|
101
192
|
|
|
102
|
-
This is what will be displayed
|
|
193
|
+
This is what will be displayed within your element. Use the `Html` to define your content.
|
|
103
194
|
|
|
104
|
-
|
|
195
|
+
```js
|
|
196
|
+
render(Html, store) {
|
|
197
|
+
Html`
|
|
198
|
+
<h1>
|
|
199
|
+
Last updated at ${new Date().toLocaleTimeString()}
|
|
200
|
+
</h1>
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The second argument `store` contains the value returned from your store function (if using `setup()`).
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Html
|
|
105
210
|
|
|
106
211
|
The primary operation is to describe the complete inner content of the element.
|
|
107
212
|
|
|
108
213
|
```js
|
|
109
|
-
render(Html,store){
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}// END render
|
|
214
|
+
render(Html, store) {
|
|
215
|
+
Html`
|
|
216
|
+
<h1>
|
|
217
|
+
Last updated at ${new Date().toLocaleTimeString()}
|
|
218
|
+
</h1>
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
117
221
|
```
|
|
118
222
|
|
|
119
223
|
The `Html` has a primary operation and two utilities: `.wire` & `.lite`
|
|
120
224
|
|
|
121
225
|
---
|
|
122
226
|
|
|
123
|
-
|
|
227
|
+
## Html.wire
|
|
228
|
+
|
|
229
|
+
Create reusable sub-elements with object/id binding for efficient rendering.
|
|
124
230
|
|
|
125
|
-
The
|
|
231
|
+
The wire takes two arguments `Html.wire(obj, id)`:
|
|
126
232
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
2. a string to identify the markup used. Allowing the markup template to be generated only once.
|
|
233
|
+
1. A reference object to match with the created node, allowing reuse of the existing node
|
|
234
|
+
2. A string to identify the markup used, allowing the template to be generated only once
|
|
130
235
|
|
|
131
|
-
Example
|
|
236
|
+
### Example: Rendering a List
|
|
132
237
|
|
|
133
238
|
```js
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
239
|
+
Html`
|
|
240
|
+
<ul>
|
|
241
|
+
${users.map((user) => Html.wire(user, ':user_list_item')`<li>${user.name}</li>`)}
|
|
242
|
+
</ul>
|
|
243
|
+
`;
|
|
139
244
|
```
|
|
140
245
|
|
|
141
|
-
|
|
246
|
+
### Anti-pattern: Inlining Markup as Strings
|
|
142
247
|
|
|
143
|
-
BAD example
|
|
248
|
+
**BAD example:** ✗
|
|
144
249
|
|
|
145
250
|
```js
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
251
|
+
Html`
|
|
252
|
+
<ul>
|
|
253
|
+
${users.map((user) => `<li>${user.name}</li>`)}
|
|
254
|
+
</ul>
|
|
255
|
+
`;
|
|
151
256
|
```
|
|
152
257
|
|
|
153
|
-
This
|
|
258
|
+
This creates a new node for every element on every render, causing:
|
|
154
259
|
|
|
155
|
-
|
|
260
|
+
- **Negative impact on performance**
|
|
261
|
+
- **Output will not be sanitized** - potential XSS vulnerability
|
|
156
262
|
|
|
157
|
-
|
|
263
|
+
### Block Syntax
|
|
158
264
|
|
|
159
|
-
The
|
|
265
|
+
The Html function supports block syntax for iteration and conditionals directly in tagged template literals:
|
|
160
266
|
|
|
161
|
-
|
|
267
|
+
| Syntax | Description |
|
|
268
|
+
| ------------------------------------- | --------------------- |
|
|
269
|
+
| `{+each ${array}}...{-each}` | Iterate over arrays |
|
|
270
|
+
| `{+if ${condition}}...{-if}` | Conditional rendering |
|
|
271
|
+
| `{+if ${condition}}...{else}...{-if}` | Conditional with else |
|
|
272
|
+
| `{+unless ${condition}}...{-unless}` | Negated conditional |
|
|
162
273
|
|
|
163
|
-
|
|
274
|
+
#### {+each} - Iteration
|
|
164
275
|
|
|
165
|
-
|
|
166
|
-
console.log("selected time "+dateText)
|
|
167
|
-
} // END onSelect
|
|
276
|
+
For cleaner list rendering, use the `{+each}...{-each}` syntax:
|
|
168
277
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return {
|
|
173
|
-
any: inputElem,
|
|
174
|
-
once:true
|
|
175
|
-
}
|
|
176
|
-
} // END Date
|
|
278
|
+
```js
|
|
279
|
+
Html`<ul>{+each ${users}}<li>{name}</li>{-each}</ul>`;
|
|
280
|
+
```
|
|
177
281
|
|
|
178
|
-
|
|
179
|
-
Html` Pick a date ${{Date:Html.lite}} `
|
|
180
|
-
} // END render
|
|
282
|
+
This is equivalent to:
|
|
181
283
|
|
|
284
|
+
```js
|
|
285
|
+
Html`<ul>${users.map((user) => Html.wire(user, ':id')`<li>${user.name}</li>`)}</ul>`;
|
|
182
286
|
```
|
|
183
287
|
|
|
184
|
-
|
|
288
|
+
The `{+each}` syntax automatically calls `Html.wire()` for each item, ensuring efficient DOM reuse.
|
|
185
289
|
|
|
290
|
+
**Available variables inside {+each}:**
|
|
186
291
|
|
|
187
|
-
|
|
292
|
+
| Syntax | Description |
|
|
293
|
+
| -------------------- | ----------------------------------------- |
|
|
294
|
+
| `{name}` | Access item property |
|
|
295
|
+
| `{address.city}` | Nested property access |
|
|
296
|
+
| `{...}` or `{ ... }` | Current item value (see formatting below) |
|
|
297
|
+
| `{@}` | Current array index (0-based) |
|
|
188
298
|
|
|
189
|
-
|
|
299
|
+
**Formatting rules for `{...}` output:**
|
|
190
300
|
|
|
191
|
-
|
|
301
|
+
| Type | Output |
|
|
302
|
+
| ----------------------------------- | ----------------------------------------------------- |
|
|
303
|
+
| Primitive (string, number, boolean) | `toString()` and HTML escaped |
|
|
304
|
+
| Array | `.join(",")` |
|
|
305
|
+
| Object | `JSON.stringify()` |
|
|
306
|
+
| Function | Called with no args, return value follows these rules |
|
|
192
307
|
|
|
193
|
-
|
|
308
|
+
**Examples:**
|
|
194
309
|
|
|
195
310
|
```js
|
|
196
|
-
//
|
|
311
|
+
// Multiple properties
|
|
312
|
+
Html`<ul>{+each ${users}}<li>{name} ({age})</li>{-each}</ul>`;
|
|
313
|
+
|
|
314
|
+
// Using index
|
|
315
|
+
Html`<ol>{+each ${items}}<li>{@}: {title}</li>{-each}</ol>`;
|
|
316
|
+
|
|
317
|
+
// Nested arrays with {+each {property}}
|
|
318
|
+
const categories = [
|
|
319
|
+
{ name: 'Fruits', items: [{ title: 'Apple' }, { title: 'Banana' }] },
|
|
320
|
+
{ name: 'Veggies', items: [{ title: 'Carrot' }] },
|
|
321
|
+
];
|
|
322
|
+
Html`
|
|
323
|
+
{+each ${categories}}
|
|
324
|
+
<section>
|
|
325
|
+
<h3>{name}</h3>
|
|
326
|
+
<ul>{+each {items}}<li>{title}</li>{-each}</ul>
|
|
327
|
+
</section>
|
|
328
|
+
{-each}
|
|
329
|
+
`;
|
|
330
|
+
```
|
|
197
331
|
|
|
198
|
-
|
|
332
|
+
#### {+if} - Conditionals
|
|
199
333
|
|
|
200
|
-
|
|
201
|
-
const onStoreChange = attachStore(getMouseValues)
|
|
334
|
+
Render content based on a condition:
|
|
202
335
|
|
|
203
|
-
|
|
204
|
-
|
|
336
|
+
```js
|
|
337
|
+
Html`{+if ${isLoggedIn}}<p>Welcome back!</p>{-if}`;
|
|
205
338
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}// END setup
|
|
339
|
+
// With else
|
|
340
|
+
Html`{+if ${isLoggedIn}}<p>Welcome back!</p>{else}<p>Please log in</p>{-if}`;
|
|
209
341
|
```
|
|
210
342
|
|
|
211
|
-
####
|
|
343
|
+
#### {+unless} - Negated Conditionals
|
|
212
344
|
|
|
213
|
-
|
|
345
|
+
Render content when condition is falsy (opposite of {+if}):
|
|
214
346
|
|
|
215
347
|
```js
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
348
|
+
Html`{+unless ${hasErrors}}<p>Form is valid</p>{-unless}`;
|
|
349
|
+
|
|
350
|
+
// With else
|
|
351
|
+
Html`{+unless ${isValid}}Invalid input!{else}Looking good!{-unless}`;
|
|
219
352
|
```
|
|
220
353
|
|
|
221
|
-
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## Html.lite
|
|
222
357
|
|
|
223
|
-
|
|
358
|
+
Create once-off sub-elements for integrating external libraries.
|
|
359
|
+
|
|
360
|
+
### Example: Wrapping jQuery DatePicker
|
|
224
361
|
|
|
225
362
|
```js
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
363
|
+
customElements.define(
|
|
364
|
+
'date-picker',
|
|
365
|
+
class extends hyperElement {
|
|
366
|
+
onSelect(dateText, inst) {
|
|
367
|
+
console.log('selected time ' + dateText);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
Date(lite) {
|
|
371
|
+
const inputElem = lite`<input type="text"/>`;
|
|
372
|
+
$(inputElem).datepicker({ onSelect: this.onSelect });
|
|
373
|
+
return {
|
|
374
|
+
any: inputElem,
|
|
375
|
+
once: true,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
render(Html) {
|
|
380
|
+
Html`Pick a date ${{ Date: Html.lite }}`;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
);
|
|
229
384
|
```
|
|
230
385
|
|
|
231
|
-
|
|
386
|
+
The `once: true` option ensures the fragment is only generated once, preventing the datepicker from being reinitialized on every render.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## setup
|
|
232
391
|
|
|
233
|
-
|
|
392
|
+
The `setup` function wires up an external data-source. This is done with the `attachStore` argument that binds a data source to your renderer.
|
|
234
393
|
|
|
235
394
|
```js
|
|
236
|
-
|
|
237
|
-
|
|
395
|
+
setup(attachStore) {
|
|
396
|
+
// the getMouseValues function will be called before each render and passed to render
|
|
397
|
+
const onStoreChange = attachStore(getMouseValues);
|
|
238
398
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const ws = new WebSocket("ws://127.0.0.1/data");
|
|
399
|
+
// call onStoreChange on every mouse event
|
|
400
|
+
onMouseMove(onStoreChange);
|
|
242
401
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
402
|
+
// cleanup logic
|
|
403
|
+
return () => console.warn('On remove, do component cleanup here');
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Live Example of [attach a store](https://codepen.io/codemeasandwich/pen/VOQWeN)**
|
|
247
408
|
|
|
248
|
-
|
|
249
|
-
return ws.close.bind(ws)
|
|
250
|
-
}// END setup
|
|
409
|
+
### Re-rendering Without a Data Source
|
|
251
410
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
411
|
+
You can trigger re-renders without any external data:
|
|
412
|
+
|
|
413
|
+
```js
|
|
414
|
+
setup(attachStore) {
|
|
415
|
+
setInterval(attachStore(), 1000); // re-render every second
|
|
416
|
+
}
|
|
255
417
|
```
|
|
256
418
|
|
|
257
|
-
|
|
419
|
+
### Set Initial Values
|
|
258
420
|
|
|
259
|
-
|
|
421
|
+
Pass static data to every render:
|
|
260
422
|
|
|
261
|
-
|
|
423
|
+
```js
|
|
424
|
+
setup(attachStore) {
|
|
425
|
+
attachStore({ max_levels: 3 }); // passed to every render
|
|
426
|
+
}
|
|
427
|
+
```
|
|
262
428
|
|
|
263
|
-
|
|
429
|
+
### Cleanup on Removal
|
|
430
|
+
|
|
431
|
+
Return a function from `setup` to run cleanup when the element is removed from the DOM:
|
|
264
432
|
|
|
265
433
|
```js
|
|
266
|
-
setup(attachStore){
|
|
434
|
+
setup(attachStore) {
|
|
435
|
+
let newSocketValue;
|
|
436
|
+
const onStoreChange = attachStore(() => newSocketValue);
|
|
437
|
+
const ws = new WebSocket('ws://127.0.0.1/data');
|
|
267
438
|
|
|
268
|
-
|
|
439
|
+
ws.onmessage = ({ data }) => {
|
|
440
|
+
newSocketValue = JSON.parse(data);
|
|
441
|
+
onStoreChange();
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
// Return cleanup function
|
|
445
|
+
return ws.close.bind(ws);
|
|
446
|
+
}
|
|
447
|
+
```
|
|
269
448
|
|
|
270
|
-
|
|
271
|
-
setInterval(onStoreChange, 1000); // update every second (update "the time is now ...")
|
|
449
|
+
### Multiple Subscriptions
|
|
272
450
|
|
|
273
|
-
|
|
451
|
+
You can trigger re-renders from multiple sources:
|
|
274
452
|
|
|
453
|
+
```js
|
|
454
|
+
setup(attachStore) {
|
|
455
|
+
const onStoreChange = attachStore(user);
|
|
456
|
+
|
|
457
|
+
mobx.autorun(onStoreChange); // update when changed (real-time feedback)
|
|
458
|
+
setInterval(onStoreChange, 1000); // update every second (update "the time is now ...")
|
|
459
|
+
}
|
|
275
460
|
```
|
|
276
461
|
|
|
277
462
|
---
|
|
278
463
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
* **this.attrs** : the attributes on the tag `<my-elem min="0" max="10" />` = `{ min:0, max:10 }`
|
|
282
|
-
* Casting types supported: `Number`
|
|
283
|
-
* **this.store** : the value returned from the store function. *!only updated before each render*
|
|
284
|
-
* **this.wrappedContent** : the text content embedded between your tag `<my-elem>Hi!</my-elem>` = `"Hi!"`
|
|
285
|
-
* **this.element** : a reference to your created element
|
|
286
|
-
* **this.dataset**: this allows reading and writing to all the custom data attributes `data-*` set on the element.
|
|
287
|
-
* Data will be parsed to try and cast them to Javascript types
|
|
288
|
-
* Casting types supported: `Object`, `Array`, `Number` & `Boolean`
|
|
289
|
-
* `dataset` is a **live reflection**. Changes on this object will update matching data attribute on its element.
|
|
290
|
-
* e.g. `<my-elem data-users='["ann","bob"]'></my-elem>` to `this.dataset.users // ["ann","bob"]`
|
|
291
|
-
* ⚠ For performance! The `dataset` works by reference. To update an attribute you must use **assignment** on the `dataset`
|
|
292
|
-
* Bad: `this.dataset.user.name = ""` ✗
|
|
293
|
-
* Good: `this.dataset.user = {name:""}` ✓
|
|
464
|
+
## this
|
|
294
465
|
|
|
295
|
-
|
|
466
|
+
Available properties and methods on `this`:
|
|
296
467
|
|
|
297
|
-
|
|
468
|
+
| Property | Description |
|
|
469
|
+
| --------------------- | --------------------------------------------------------------------------- |
|
|
470
|
+
| `this.attrs` | Attributes on the tag. `<my-elem min="0" max="10" />` = `{ min:0, max:10 }` |
|
|
471
|
+
| `this.store` | Value returned from the store function. _Only updated before each render_ |
|
|
472
|
+
| `this.wrappedContent` | Text content between your tags. `<my-elem>Hi!</my-elem>` = `"Hi!"` |
|
|
473
|
+
| `this.element` | Reference to your created DOM element |
|
|
474
|
+
| `this.dataset` | Read/write access to all `data-*` attributes |
|
|
298
475
|
|
|
299
|
-
###
|
|
476
|
+
### this.attrs
|
|
300
477
|
|
|
301
|
-
|
|
478
|
+
Attributes are automatically type-coerced:
|
|
302
479
|
|
|
303
|
-
|
|
480
|
+
| Input | Output | Type |
|
|
481
|
+
| --------- | --------- | ------ |
|
|
482
|
+
| `"42"` | `42` | Number |
|
|
483
|
+
| `"3.14"` | `3.14` | Number |
|
|
484
|
+
| `"hello"` | `"hello"` | String |
|
|
304
485
|
|
|
305
|
-
|
|
486
|
+
### this.dataset
|
|
306
487
|
|
|
307
|
-
|
|
488
|
+
The dataset provides proxied access to `data-*` attributes with automatic JSON parsing:
|
|
308
489
|
|
|
309
|
-
|
|
490
|
+
| Attribute Value | `this.dataset` Value | Type |
|
|
491
|
+
| ------------------------ | -------------------- | ------- |
|
|
492
|
+
| `data-count="42"` | `42` | Number |
|
|
493
|
+
| `data-active="true"` | `true` | Boolean |
|
|
494
|
+
| `data-active="false"` | `false` | Boolean |
|
|
495
|
+
| `data-users='["a","b"]'` | `["a", "b"]` | Array |
|
|
496
|
+
| `data-config='{"x":1}'` | `{ x: 1 }` | Object |
|
|
497
|
+
|
|
498
|
+
**Example:**
|
|
310
499
|
|
|
311
500
|
```html
|
|
312
|
-
<
|
|
313
|
-
<users-elem />
|
|
501
|
+
<my-elem data-users='["ann","bob"]'></my-elem>
|
|
314
502
|
```
|
|
315
503
|
|
|
316
|
-
|
|
504
|
+
```js
|
|
505
|
+
this.dataset.users; // ["ann", "bob"]
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
The `dataset` is a **live reflection**. Changes update the matching data attribute on the element:
|
|
317
509
|
|
|
318
510
|
```js
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
511
|
+
this.dataset.user = { name: 'Alice' }; // Updates data-user attribute
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Advanced Attributes
|
|
325
517
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
518
|
+
### Dynamic Attributes with Custom-element Children
|
|
519
|
+
|
|
520
|
+
Being able to set attributes at run-time should be the same for dealing with a native element and ones defined by hyper-element.
|
|
521
|
+
|
|
522
|
+
**⚠ To support dynamic attributes on custom elements YOU MUST USE `customElements.define` which requires native ES6 support! Use `/build/hyperElement.min.js`.**
|
|
523
|
+
|
|
524
|
+
This is what allows for the passing any dynamic attributes from parent to child custom element! You can also pass a `function` to a child element (that extends hyperElement).
|
|
525
|
+
|
|
526
|
+
**Example:**
|
|
527
|
+
|
|
528
|
+
```js
|
|
529
|
+
window.customElements.define(
|
|
530
|
+
'a-user',
|
|
531
|
+
class extends hyperElement {
|
|
532
|
+
render(Html) {
|
|
533
|
+
const onClick = () => this.attrs.hi('Hello from ' + this.attrs.name);
|
|
534
|
+
Html`${this.attrs.name} <button onclick=${onClick}>Say hi!</button>`;
|
|
535
|
+
}
|
|
329
536
|
}
|
|
330
|
-
|
|
331
|
-
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
window.customElements.define(
|
|
540
|
+
'users-elem',
|
|
541
|
+
class extends hyperElement {
|
|
542
|
+
onHi(val) {
|
|
543
|
+
console.log('hi was clicked', val);
|
|
544
|
+
}
|
|
545
|
+
render(Html) {
|
|
546
|
+
Html`<a-user hi=${this.onHi} name="Beckett" />`;
|
|
547
|
+
}
|
|
332
548
|
}
|
|
333
|
-
|
|
549
|
+
);
|
|
334
550
|
```
|
|
335
551
|
|
|
336
|
-
|
|
552
|
+
**Live Example of passing an [onclick to a child element](https://codepen.io/codemeasandwich/pen/rgdvPX)**
|
|
337
553
|
|
|
338
|
-
|
|
339
|
-
<users-elem>
|
|
340
|
-
<a-user update="fn-bgzvylhphgvpwtv" name="Beckett">
|
|
341
|
-
Beckett <button>Say hi!</button>
|
|
342
|
-
</a-user>
|
|
343
|
-
</users-elem>
|
|
344
|
-
```
|
|
554
|
+
---
|
|
345
555
|
|
|
346
|
-
|
|
556
|
+
# Templates
|
|
347
557
|
|
|
348
|
-
|
|
558
|
+
Unlike standard Custom Elements which typically discard or replace their innerHTML, hyper-element's template system **preserves** the markup inside your element and uses it as a reusable template. This means your custom element primarily holds logic, while the template markup between the tags defines how data should be rendered.
|
|
349
559
|
|
|
350
560
|
To enable templates:
|
|
351
561
|
|
|
352
|
-
1. Add
|
|
562
|
+
1. Add a `template` attribute to your custom element
|
|
353
563
|
2. Define the template markup within your element
|
|
564
|
+
3. Call `Html.template(data)` in your render method to populate the template
|
|
354
565
|
|
|
355
566
|
**Example:**
|
|
356
567
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
```Html
|
|
360
|
-
<my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]' >
|
|
568
|
+
```html
|
|
569
|
+
<my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]'>
|
|
361
570
|
<div>
|
|
362
571
|
<a href="{url}">{name}</a>
|
|
363
572
|
</div>
|
|
364
573
|
</my-list>
|
|
365
574
|
```
|
|
366
575
|
|
|
367
|
-
Implementation:
|
|
368
|
-
|
|
369
576
|
```js
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
577
|
+
customElements.define(
|
|
578
|
+
'my-list',
|
|
579
|
+
class extends hyperElement {
|
|
580
|
+
render(Html) {
|
|
581
|
+
Html`${this.dataset.json.map((user) => Html.template(user))}`;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
);
|
|
378
585
|
```
|
|
379
586
|
|
|
380
587
|
Output:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
</
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
</
|
|
588
|
+
|
|
589
|
+
```html
|
|
590
|
+
<my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]'>
|
|
591
|
+
<div>
|
|
592
|
+
<a href="">ann</a>
|
|
593
|
+
</div>
|
|
594
|
+
<div>
|
|
595
|
+
<a href="">bob</a>
|
|
596
|
+
</div>
|
|
389
597
|
</my-list>
|
|
390
598
|
```
|
|
391
599
|
|
|
392
|
-
|
|
600
|
+
**Live Example of using [templates](https://codepen.io/codemeasandwich/pen/LoQLrK)**
|
|
601
|
+
|
|
602
|
+
---
|
|
393
603
|
|
|
394
|
-
|
|
604
|
+
## Basic Template Syntax
|
|
395
605
|
|
|
396
|
-
|
|
606
|
+
| Syntax | Description |
|
|
607
|
+
| ---------------------------------- | ------------------------------------- |
|
|
608
|
+
| `{variable}` | Simple interpolation |
|
|
609
|
+
| `{+if condition}...{-if}` | Conditional rendering |
|
|
610
|
+
| `{+if condition}...{else}...{-if}` | Conditional with else |
|
|
611
|
+
| `{+unless condition}...{-unless}` | Negative conditional (opposite of if) |
|
|
612
|
+
| `{+each items}...{-each}` | Iteration over arrays |
|
|
613
|
+
| `{@}` | Current index in each loop (0-based) |
|
|
397
614
|
|
|
398
|
-
|
|
615
|
+
---
|
|
399
616
|
|
|
400
|
-
|
|
617
|
+
## Conditionals: {+if}
|
|
401
618
|
|
|
402
|
-
|
|
403
|
-
* **once:** Only generate the fragment once.
|
|
404
|
-
* Default: `false`. The fragment function will be run on every render!
|
|
619
|
+
Show content based on a condition:
|
|
405
620
|
|
|
406
|
-
|
|
621
|
+
```html
|
|
622
|
+
<status-elem template>{+if active}Online{else}Offline{-if}</status-elem>
|
|
623
|
+
```
|
|
407
624
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
625
|
+
```js
|
|
626
|
+
customElements.define(
|
|
627
|
+
'status-elem',
|
|
628
|
+
class extends hyperElement {
|
|
629
|
+
render(Html) {
|
|
630
|
+
Html`${Html.template({ active: true })}`;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
);
|
|
634
|
+
```
|
|
413
635
|
|
|
414
|
-
|
|
636
|
+
Output: `Online`
|
|
415
637
|
|
|
416
|
-
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Negation: {+unless}
|
|
641
|
+
|
|
642
|
+
Show content when condition is falsy (opposite of +if):
|
|
643
|
+
|
|
644
|
+
```html
|
|
645
|
+
<warning-elem template>{+unless valid}Invalid input!{-unless}</warning-elem>
|
|
646
|
+
```
|
|
417
647
|
|
|
418
648
|
```js
|
|
419
|
-
|
|
649
|
+
customElements.define(
|
|
650
|
+
'warning-elem',
|
|
651
|
+
class extends hyperElement {
|
|
652
|
+
render(Html) {
|
|
653
|
+
Html`${Html.template({ valid: false })}`;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Output: `Invalid input!`
|
|
420
660
|
|
|
421
|
-
|
|
422
|
-
return {
|
|
661
|
+
---
|
|
423
662
|
|
|
424
|
-
|
|
663
|
+
## Iteration: {+each}
|
|
425
664
|
|
|
426
|
-
|
|
665
|
+
Loop over arrays:
|
|
427
666
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
667
|
+
```html
|
|
668
|
+
<list-elem template>
|
|
669
|
+
<ul>
|
|
670
|
+
{+each items}
|
|
671
|
+
<li>{name}</li>
|
|
672
|
+
{-each}
|
|
673
|
+
</ul>
|
|
674
|
+
</list-elem>
|
|
675
|
+
```
|
|
434
676
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
677
|
+
```js
|
|
678
|
+
customElements.define(
|
|
679
|
+
'list-elem',
|
|
680
|
+
class extends hyperElement {
|
|
681
|
+
render(Html) {
|
|
682
|
+
Html`${Html.template({ items: [{ name: 'Ann' }, { name: 'Bob' }] })}`;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
);
|
|
440
686
|
```
|
|
441
687
|
|
|
442
688
|
Output:
|
|
443
689
|
|
|
444
690
|
```html
|
|
445
|
-
<
|
|
446
|
-
<
|
|
447
|
-
</
|
|
691
|
+
<ul>
|
|
692
|
+
<li>Ann</li>
|
|
693
|
+
<li>Bob</li>
|
|
694
|
+
</ul>
|
|
448
695
|
```
|
|
449
|
-
|
|
696
|
+
|
|
697
|
+
### Special Variables in {+each}
|
|
698
|
+
|
|
699
|
+
- `{@}` - The current index (0-based)
|
|
450
700
|
|
|
451
701
|
```html
|
|
452
|
-
<
|
|
453
|
-
<h2> you have 635 friends </h2>
|
|
454
|
-
</my-friends>
|
|
702
|
+
<nums-elem template>{+each numbers}{@}: {number}, {-each}</nums-elem>
|
|
455
703
|
```
|
|
456
704
|
|
|
457
|
-
|
|
705
|
+
```js
|
|
706
|
+
customElements.define(
|
|
707
|
+
'nums-elem',
|
|
708
|
+
class extends hyperElement {
|
|
709
|
+
render(Html) {
|
|
710
|
+
Html`${Html.template({ numbers: ['a', 'b', 'c'] })}`;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
);
|
|
714
|
+
```
|
|
458
715
|
|
|
459
|
-
|
|
716
|
+
Output: `0: a, 1: b, 2: c, `
|
|
460
717
|
|
|
461
|
-
|
|
718
|
+
---
|
|
462
719
|
|
|
463
|
-
|
|
464
|
-
* `Foo(values){ return{ template:"<p>{txt}</p>", values:{txt:"Ipsum"} }}`
|
|
465
|
-
* with `` Html`${{Foo:{}}}` ``
|
|
720
|
+
# Fragments
|
|
466
721
|
|
|
467
|
-
|
|
468
|
-
* `Foo(values){ return{ template:"<p>{txt}</p>" }}`
|
|
469
|
-
* with `` Html`${{Foo:{txt:"Ipsum"}}}` ``
|
|
722
|
+
Fragments are pieces of content that can be loaded _asynchronously_.
|
|
470
723
|
|
|
471
|
-
|
|
724
|
+
You define one with a class property starting with a **capital letter**.
|
|
472
725
|
|
|
473
|
-
|
|
726
|
+
The fragment function should return an object with:
|
|
474
727
|
|
|
475
|
-
|
|
728
|
+
- **placeholder:** the placeholder to show while resolving
|
|
729
|
+
- **once:** Only generate the fragment once (default: `false`)
|
|
476
730
|
|
|
477
|
-
**
|
|
731
|
+
And **one** of the following as the result:
|
|
732
|
+
|
|
733
|
+
- **text:** An escaped string to output
|
|
734
|
+
- **any:** Any type of content
|
|
735
|
+
- **html:** A html string to output **(not sanitised)**
|
|
736
|
+
- **template:** A template string to use **(is sanitised)**
|
|
478
737
|
|
|
479
|
-
|
|
738
|
+
**Example:**
|
|
480
739
|
|
|
481
740
|
```js
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
741
|
+
customElements.define(
|
|
742
|
+
'my-friends',
|
|
743
|
+
class extends hyperElement {
|
|
744
|
+
FriendCount(user) {
|
|
745
|
+
return {
|
|
746
|
+
once: true,
|
|
747
|
+
placeholder: 'loading your number of friends',
|
|
748
|
+
text: fetch('/user/' + user.userId + '/friends')
|
|
749
|
+
.then((b) => b.json())
|
|
750
|
+
.then((friends) => `you have ${friends.count} friends`)
|
|
751
|
+
.catch((err) => 'problem loading friends'),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
render(Html) {
|
|
756
|
+
const userId = this.attrs.myId;
|
|
757
|
+
Html`<h2> ${{ FriendCount: userId }} </h2>`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
);
|
|
496
761
|
```
|
|
497
762
|
|
|
498
|
-
|
|
763
|
+
**Live Example of using an [asynchronous fragment](https://codepen.io/codemeasandwich/pen/MdQrVd)**
|
|
499
764
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
# Styling
|
|
768
|
+
|
|
769
|
+
Supports an object as the style attribute. Compatible with React's implementation.
|
|
770
|
+
|
|
771
|
+
**Example:** of centering an element
|
|
772
|
+
|
|
773
|
+
```js
|
|
774
|
+
render(Html) {
|
|
775
|
+
const style = {
|
|
776
|
+
position: 'absolute',
|
|
777
|
+
top: '50%',
|
|
778
|
+
left: '50%',
|
|
779
|
+
marginRight: '-50%',
|
|
780
|
+
transform: 'translate(-50%, -50%)',
|
|
781
|
+
};
|
|
782
|
+
Html`<div style=${style}> center </div>`;
|
|
783
|
+
}
|
|
504
784
|
```
|
|
505
785
|
|
|
506
|
-
|
|
786
|
+
**Live Example of [styling](https://codepen.io/codemeasandwich/pen/RmQVKY)**
|
|
507
787
|
|
|
508
|
-
|
|
788
|
+
---
|
|
509
789
|
|
|
510
|
-
|
|
790
|
+
# Connecting to a Data Store
|
|
511
791
|
|
|
512
|
-
|
|
792
|
+
hyper-element integrates with any state management library via `setup()`. The pattern is:
|
|
513
793
|
|
|
514
|
-
|
|
794
|
+
1. Call `attachStore()` with a function that returns your state
|
|
795
|
+
2. Subscribe to your store and call the returned function when state changes
|
|
796
|
+
|
|
797
|
+
## Backbone
|
|
515
798
|
|
|
516
799
|
```js
|
|
517
|
-
|
|
800
|
+
var user = new (Backbone.Model.extend({
|
|
801
|
+
defaults: {
|
|
802
|
+
name: 'Guest User',
|
|
803
|
+
},
|
|
804
|
+
}))();
|
|
805
|
+
|
|
806
|
+
customElements.define(
|
|
807
|
+
'my-profile',
|
|
808
|
+
class extends hyperElement {
|
|
809
|
+
setup(attachStore) {
|
|
810
|
+
user.on('change', attachStore(user.toJSON.bind(user)));
|
|
811
|
+
// OR user.on("change", attachStore(() => user.toJSON()));
|
|
812
|
+
}
|
|
518
813
|
|
|
519
|
-
|
|
814
|
+
render(Html, { name }) {
|
|
815
|
+
Html`Profile: ${name}`;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
);
|
|
819
|
+
```
|
|
520
820
|
|
|
521
|
-
|
|
522
|
-
.then(b => b.json())
|
|
523
|
-
.then(friends => ({
|
|
524
|
-
template:`you have {count} friends`,
|
|
525
|
-
values:{count:friends.count}
|
|
526
|
-
})
|
|
527
|
-
}) // END .then
|
|
528
|
-
.catch(err=>({ template:`problem loading friends` })
|
|
821
|
+
## MobX
|
|
529
822
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
823
|
+
```js
|
|
824
|
+
const user = observable({
|
|
825
|
+
name: 'Guest User',
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
customElements.define(
|
|
829
|
+
'my-profile',
|
|
830
|
+
class extends hyperElement {
|
|
831
|
+
setup(attachStore) {
|
|
832
|
+
mobx.autorun(attachStore(user));
|
|
833
|
+
}
|
|
536
834
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
835
|
+
render(Html, { name }) {
|
|
836
|
+
Html`Profile: ${name}`;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
);
|
|
542
840
|
```
|
|
543
841
|
|
|
544
|
-
|
|
842
|
+
## Redux
|
|
545
843
|
|
|
546
|
-
|
|
844
|
+
```js
|
|
845
|
+
customElements.define(
|
|
846
|
+
'my-profile',
|
|
847
|
+
class extends hyperElement {
|
|
848
|
+
setup(attachStore) {
|
|
849
|
+
store.subscribe(attachStore(store.getState));
|
|
850
|
+
}
|
|
547
851
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
852
|
+
render(Html, { user }) {
|
|
853
|
+
Html`Profile: ${user.name}`;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
);
|
|
552
857
|
```
|
|
553
858
|
|
|
859
|
+
---
|
|
554
860
|
|
|
555
|
-
|
|
861
|
+
# Best Practices
|
|
556
862
|
|
|
557
|
-
|
|
863
|
+
## Always Use Html.wire for Lists
|
|
558
864
|
|
|
559
|
-
|
|
865
|
+
When rendering lists, always use `Html.wire()` to ensure proper DOM reuse and prevent XSS vulnerabilities:
|
|
560
866
|
|
|
561
867
|
```js
|
|
868
|
+
// GOOD - Safe and efficient
|
|
869
|
+
Html`<ul>${users.map((u) => Html.wire(u, ':item')`<li>${u.name}</li>`)}</ul>`;
|
|
562
870
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
871
|
+
// BAD - XSS vulnerability and poor performance
|
|
872
|
+
Html`<ul>${users.map((u) => `<li>${u.name}</li>`)}</ul>`;
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
## Dataset Updates Require Assignment
|
|
876
|
+
|
|
877
|
+
The `dataset` works by reference. To update an attribute you must use **assignment**:
|
|
878
|
+
|
|
879
|
+
```js
|
|
880
|
+
// BAD - mutation doesn't trigger attribute update
|
|
881
|
+
this.dataset.user.name = '';
|
|
572
882
|
|
|
883
|
+
// GOOD - assignment triggers attribute update
|
|
884
|
+
this.dataset.user = { name: '' };
|
|
573
885
|
```
|
|
574
886
|
|
|
575
|
-
|
|
887
|
+
## Type Coercion Reference
|
|
888
|
+
|
|
889
|
+
| Source | Supported Types |
|
|
890
|
+
| -------------- | ------------------------------ |
|
|
891
|
+
| `this.attrs` | Number |
|
|
892
|
+
| `this.dataset` | Object, Array, Number, Boolean |
|
|
576
893
|
|
|
577
|
-
##
|
|
894
|
+
## Cleanup Resources in setup()
|
|
895
|
+
|
|
896
|
+
Always return a cleanup function when using resources that need disposal:
|
|
578
897
|
|
|
579
898
|
```js
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
899
|
+
setup(attachStore) {
|
|
900
|
+
const interval = setInterval(attachStore(), 1000);
|
|
901
|
+
return () => clearInterval(interval); // Cleanup on removal
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
---
|
|
906
|
+
|
|
907
|
+
# Development
|
|
908
|
+
|
|
909
|
+
## Prerequisites
|
|
910
|
+
|
|
911
|
+
- **Node.js** 20 or higher
|
|
912
|
+
- **npm** (comes with Node.js)
|
|
913
|
+
|
|
914
|
+
## Setup
|
|
585
915
|
|
|
916
|
+
1. Clone the repository:
|
|
586
917
|
|
|
587
|
-
|
|
918
|
+
```bash
|
|
919
|
+
git clone https://github.com/codemeasandwich/hyper-element.git
|
|
920
|
+
cd hyper-element
|
|
921
|
+
```
|
|
588
922
|
|
|
589
|
-
|
|
590
|
-
user.on("change",attachStore(user.toJSON.bind(user)));
|
|
591
|
-
// OR user.on("change",attachStore(()=>user.toJSON()));
|
|
592
|
-
}//END setup
|
|
923
|
+
2. Install dependencies:
|
|
593
924
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
925
|
+
```bash
|
|
926
|
+
npm install
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
This also installs the pre-commit hooks automatically via the `prepare` script.
|
|
930
|
+
|
|
931
|
+
## Available Scripts
|
|
932
|
+
|
|
933
|
+
| Command | Description |
|
|
934
|
+
| --------------------- | ------------------------------------------------- |
|
|
935
|
+
| `npm run build` | Build minified production bundle with source maps |
|
|
936
|
+
| `npm test` | Run Playwright tests with coverage |
|
|
937
|
+
| `npm run test:ui` | Run tests with Playwright UI for debugging |
|
|
938
|
+
| `npm run test:headed` | Run tests in headed browser mode |
|
|
939
|
+
| `npm run kitchensink` | Start local dev server for examples |
|
|
940
|
+
| `npm run lint` | Run ESLint to check for code issues |
|
|
941
|
+
| `npm run format` | Check Prettier formatting |
|
|
942
|
+
| `npm run format:fix` | Auto-fix Prettier formatting issues |
|
|
943
|
+
| `npm run release` | Run the release script (maintainers only) |
|
|
944
|
+
|
|
945
|
+
## Project Structure
|
|
946
|
+
|
|
947
|
+
```
|
|
948
|
+
hyper-element/
|
|
949
|
+
├── src/ # Source files (ES modules)
|
|
950
|
+
│ ├── core/ # Core utilities
|
|
951
|
+
│ ├── attributes/ # Attribute handling
|
|
952
|
+
│ ├── template/ # Template processing
|
|
953
|
+
│ ├── html/ # HTML rendering
|
|
954
|
+
│ ├── lifecycle/ # Lifecycle hooks
|
|
955
|
+
│ └── hyperElement.js # Main export
|
|
956
|
+
├── build/
|
|
957
|
+
│ ├── hyperElement.min.js # Minified production build
|
|
958
|
+
│ └── hyperElement.min.js.map
|
|
959
|
+
├── kitchensink/ # Test suite
|
|
960
|
+
│ ├── kitchensink.spec.js # Playwright test runner
|
|
961
|
+
│ └── *.html # Test case files
|
|
962
|
+
├── example/ # Example project
|
|
963
|
+
├── docs/ # Documentation
|
|
964
|
+
├── .hooks/ # Git hooks
|
|
965
|
+
│ ├── pre-commit # Main hook orchestrator
|
|
966
|
+
│ ├── commit-msg # Commit message validator
|
|
967
|
+
│ └── pre-commit.d/ # Modular validation scripts
|
|
968
|
+
└── scripts/
|
|
969
|
+
└── publish.sh # Release script
|
|
598
970
|
```
|
|
599
971
|
|
|
600
|
-
##
|
|
972
|
+
## Building
|
|
601
973
|
|
|
602
|
-
|
|
603
|
-
const user = observable({
|
|
604
|
-
name: 'Guest User'
|
|
605
|
-
})//END observable
|
|
974
|
+
The build process uses [esbuild](https://esbuild.github.io/) for fast, minimal output:
|
|
606
975
|
|
|
976
|
+
```bash
|
|
977
|
+
npm run build
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
This produces:
|
|
981
|
+
|
|
982
|
+
- `build/hyperElement.min.js` - Minified bundle (~6.2 KB)
|
|
983
|
+
- `build/hyperElement.min.js.map` - Source map for debugging
|
|
984
|
+
|
|
985
|
+
## Pre-commit Hooks
|
|
607
986
|
|
|
608
|
-
|
|
987
|
+
The project uses a modular pre-commit hook system located in `.hooks/`. When you commit, the following checks run automatically:
|
|
609
988
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
989
|
+
1. **ESLint** - Code quality checks
|
|
990
|
+
2. **Prettier** - Code formatting
|
|
991
|
+
3. **Build** - Ensures the build succeeds
|
|
992
|
+
4. **Coverage** - Enforces 100% test coverage
|
|
993
|
+
5. **JSDoc** - Documentation validation
|
|
994
|
+
6. **Docs** - Documentation completeness
|
|
613
995
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
996
|
+
If any check fails, the commit is blocked until the issue is fixed.
|
|
997
|
+
|
|
998
|
+
### Installing Hooks Manually
|
|
999
|
+
|
|
1000
|
+
If hooks weren't installed automatically:
|
|
1001
|
+
|
|
1002
|
+
```bash
|
|
1003
|
+
npm run hooks:install
|
|
618
1004
|
```
|
|
619
1005
|
|
|
620
|
-
##
|
|
1006
|
+
## Code Style
|
|
621
1007
|
|
|
622
|
-
|
|
623
|
-
|
|
1008
|
+
- **Prettier** for formatting (2-space indent, single quotes, trailing commas)
|
|
1009
|
+
- **ESLint** for code quality
|
|
1010
|
+
- All files are automatically checked on commit
|
|
624
1011
|
|
|
625
|
-
|
|
626
|
-
store.subcribe(attachStore(store.getState)
|
|
627
|
-
}// END setup
|
|
1012
|
+
Run formatting manually:
|
|
628
1013
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}// END render
|
|
632
|
-
})// END my-profile
|
|
1014
|
+
```bash
|
|
1015
|
+
npm run format:fix
|
|
633
1016
|
```
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
[
|
|
638
|
-
|
|
639
|
-
|
|
1017
|
+
|
|
1018
|
+
## Testing
|
|
1019
|
+
|
|
1020
|
+
See [kitchensink/README.md](kitchensink/README.md) for the full testing guide.
|
|
1021
|
+
|
|
1022
|
+
## Contributing
|
|
1023
|
+
|
|
1024
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
[shadow-dom]: https://developers.google.com/web/fundamentals/web-components/shadowdom
|
|
1029
|
+
[innerHTML]: https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
|
|
1030
|
+
[hyperHTML]: https://viperhtml.js.org/hyper.html
|
|
1031
|
+
[Custom Elements]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements
|
|
1032
|
+
[Test system]: https://jsfiddle.net/codemeasandwich/k25e6ufv/36/
|
|
1033
|
+
[promise]: https://scotch.io/tutorials/javascript-promises-for-dummies#understanding-promises
|