hyper-element 1.0.3 → 2.0.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 +673 -29
- package/build/hyperElement.min.js +14 -2
- package/build/hyperElement.min.js.map +3 -3
- package/index.d.ts +450 -1
- package/package.json +18 -13
package/README.md
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
[](https://github.com/codemeasandwich/hyper-element/actions/workflows/publish.yml)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://github.com/codemeasandwich/hyper-element)
|
|
8
|
+
[](https://github.com/codemeasandwich/hyper-element)
|
|
8
9
|
[](https://caniuse.com/es6)
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
A lightweight [Custom Elements] library with a fast, built-in render core. Your custom-element will react to tag attribute and store changes with efficient DOM updates.
|
|
11
12
|
|
|
12
13
|
### If you like it, please [★ it on github](https://github.com/codemeasandwich/hyper-element)
|
|
13
14
|
|
|
@@ -24,14 +25,7 @@ npm install hyper-element
|
|
|
24
25
|
```js
|
|
25
26
|
import hyperElement from 'hyper-element';
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
'my-elem',
|
|
29
|
-
class extends hyperElement {
|
|
30
|
-
render(Html) {
|
|
31
|
-
Html`Hello ${this.attrs.who}!`;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
);
|
|
28
|
+
hyperElement('my-elem', (Html, ctx) => Html`Hello ${ctx.attrs.who}!`);
|
|
35
29
|
```
|
|
36
30
|
|
|
37
31
|
### CommonJS
|
|
@@ -39,14 +33,7 @@ customElements.define(
|
|
|
39
33
|
```js
|
|
40
34
|
const hyperElement = require('hyper-element');
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
'my-elem',
|
|
44
|
-
class extends hyperElement {
|
|
45
|
-
render(Html) {
|
|
46
|
-
Html`Hello ${this.attrs.who}!`;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
);
|
|
36
|
+
hyperElement('my-elem', (Html, ctx) => Html`Hello ${ctx.attrs.who}!`);
|
|
50
37
|
```
|
|
51
38
|
|
|
52
39
|
## CDN (Browser)
|
|
@@ -57,7 +44,7 @@ For browser environments without a bundler:
|
|
|
57
44
|
<script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/build/hyperElement.min.js"></script>
|
|
58
45
|
```
|
|
59
46
|
|
|
60
|
-
The `hyperElement` class will be available globally on `window.hyperElement`.
|
|
47
|
+
The `hyperElement` class will be available globally on `window.hyperElement`.
|
|
61
48
|
|
|
62
49
|
## Browser Support
|
|
63
50
|
|
|
@@ -65,22 +52,23 @@ hyper-element requires native ES6 class support and the Custom Elements v1 API:
|
|
|
65
52
|
|
|
66
53
|
| Browser | Version |
|
|
67
54
|
| ------- | ------- |
|
|
68
|
-
| Chrome |
|
|
69
|
-
| Firefox |
|
|
70
|
-
| Safari |
|
|
71
|
-
| Edge |
|
|
55
|
+
| Chrome | 86+ |
|
|
56
|
+
| Firefox | 78+ |
|
|
57
|
+
| Safari | 14.1+ |
|
|
58
|
+
| Edge | 86+ |
|
|
72
59
|
|
|
73
60
|
For older browsers, a [Custom Elements polyfill](https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements) may be required.
|
|
74
61
|
|
|
75
62
|
## Why hyper-element
|
|
76
63
|
|
|
77
64
|
- hyper-element is fast & small
|
|
78
|
-
-
|
|
65
|
+
- Zero runtime dependencies - everything is built-in
|
|
79
66
|
- With a completely stateless approach, setting and reseting the view is trivial
|
|
80
67
|
- Simple yet powerful [Interface](#interface)
|
|
81
68
|
- Built in [template](#templates) system to customise the rendered output
|
|
82
69
|
- Inline style objects supported (similar to React)
|
|
83
70
|
- First class support for [data stores](#connecting-to-a-data-store)
|
|
71
|
+
- [Server-side rendering](#server-side-rendering-ssr) with progressive hydration
|
|
84
72
|
- Pass `function` to other custom hyper-elements via there tag attribute
|
|
85
73
|
|
|
86
74
|
# [Live Demo](https://jsfiddle.net/codemeasandwich/k25e6ufv/)
|
|
@@ -101,6 +89,7 @@ For older browsers, a [Custom Elements polyfill](https://github.com/webcomponent
|
|
|
101
89
|
|
|
102
90
|
- [Browser Support](#browser-support)
|
|
103
91
|
- [Define a Custom Element](#define-a-custom-element)
|
|
92
|
+
- [Functional API](#functional-api)
|
|
104
93
|
- [Lifecycle](#lifecycle)
|
|
105
94
|
- [Interface](#interface)
|
|
106
95
|
- [render](#render)
|
|
@@ -121,6 +110,16 @@ For older browsers, a [Custom Elements polyfill](https://github.com/webcomponent
|
|
|
121
110
|
- [Backbone](#backbone)
|
|
122
111
|
- [MobX](#mobx)
|
|
123
112
|
- [Redux](#redux)
|
|
113
|
+
- [Signals](#signals)
|
|
114
|
+
- [signal](#signal)
|
|
115
|
+
- [computed](#computed-1)
|
|
116
|
+
- [effect](#effect)
|
|
117
|
+
- [batch](#batch)
|
|
118
|
+
- [untracked](#untracked)
|
|
119
|
+
- [Server-Side Rendering (SSR)](#server-side-rendering-ssr)
|
|
120
|
+
- [Server-Side API](#server-side-api)
|
|
121
|
+
- [Client-Side Hydration](#client-side-hydration)
|
|
122
|
+
- [SSR Configuration](#ssr-configuration)
|
|
124
123
|
- [Best Practices](#best-practices)
|
|
125
124
|
- [Development](#development)
|
|
126
125
|
|
|
@@ -160,6 +159,107 @@ Output
|
|
|
160
159
|
|
|
161
160
|
---
|
|
162
161
|
|
|
162
|
+
# Functional API
|
|
163
|
+
|
|
164
|
+
In addition to class-based components, hyper-element supports a functional API that hides the class internals. This is useful for simpler components or if you prefer a more functional programming style.
|
|
165
|
+
|
|
166
|
+
## Signatures
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
// 1. Full definition with tag (auto-registers)
|
|
170
|
+
hyperElement('my-counter', {
|
|
171
|
+
setup: (ctx, onNext) => {
|
|
172
|
+
/* ... */
|
|
173
|
+
},
|
|
174
|
+
render: (Html, ctx, store) => Html`Count: ${ctx.attrs.count}`,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 2. Shorthand with tag (auto-registers)
|
|
178
|
+
hyperElement('hello-world', (Html, ctx) => Html`Hello, ${ctx.attrs.name}!`);
|
|
179
|
+
|
|
180
|
+
// 3. Definition without tag (returns class for manual registration)
|
|
181
|
+
const MyElement = hyperElement({
|
|
182
|
+
render: (Html, ctx) => Html`...`,
|
|
183
|
+
});
|
|
184
|
+
customElements.define('my-element', MyElement);
|
|
185
|
+
|
|
186
|
+
// 4. Shorthand without tag (returns class for manual registration)
|
|
187
|
+
const Simple = hyperElement((Html, ctx) => Html`Simple!`);
|
|
188
|
+
customElements.define('simple-elem', Simple);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Context Object
|
|
192
|
+
|
|
193
|
+
In the functional API, instead of using `this`, a context object (`ctx`) is passed explicitly to all functions:
|
|
194
|
+
|
|
195
|
+
| Property | Description |
|
|
196
|
+
| -------------------- | ---------------------------------------------- |
|
|
197
|
+
| `ctx.element` | The DOM element |
|
|
198
|
+
| `ctx.attrs` | Parsed attributes with automatic type coercion |
|
|
199
|
+
| `ctx.dataset` | Dataset proxy with automatic type coercion |
|
|
200
|
+
| `ctx.store` | Store value from setup |
|
|
201
|
+
| `ctx.wrappedContent` | Text content between the tags |
|
|
202
|
+
|
|
203
|
+
## Example: Counter with Setup
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
hyperElement('my-counter', {
|
|
207
|
+
setup: (ctx, onNext) => {
|
|
208
|
+
const store = { count: 0 };
|
|
209
|
+
const render = onNext(() => store);
|
|
210
|
+
|
|
211
|
+
ctx.increment = () => {
|
|
212
|
+
store.count++;
|
|
213
|
+
render();
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
handleClick: (ctx, event) => ctx.increment(),
|
|
218
|
+
|
|
219
|
+
render: (Html, ctx, store) => Html`
|
|
220
|
+
<button onclick=${ctx.handleClick}>
|
|
221
|
+
Count: ${store?.count || 0}
|
|
222
|
+
</button>
|
|
223
|
+
`,
|
|
224
|
+
});
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Example: Timer with Teardown
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
hyperElement('my-timer', {
|
|
231
|
+
setup: (ctx, onNext) => {
|
|
232
|
+
let seconds = 0;
|
|
233
|
+
const render = onNext(() => ({ seconds }));
|
|
234
|
+
|
|
235
|
+
const interval = setInterval(() => {
|
|
236
|
+
seconds++;
|
|
237
|
+
render();
|
|
238
|
+
}, 1000);
|
|
239
|
+
|
|
240
|
+
// Return cleanup function
|
|
241
|
+
return () => clearInterval(interval);
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
render: (Html, ctx, store) => Html`Elapsed: ${store?.seconds || 0}s`,
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Backward Compatibility
|
|
249
|
+
|
|
250
|
+
The functional API is fully backward compatible. Class-based components still work:
|
|
251
|
+
|
|
252
|
+
```js
|
|
253
|
+
class MyElement extends hyperElement {
|
|
254
|
+
render(Html) {
|
|
255
|
+
Html`Hello ${this.attrs.name}!`;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
customElements.define('my-element', MyElement);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
163
263
|
# Lifecycle
|
|
164
264
|
|
|
165
265
|
When a hyper-element is connected to the DOM, it goes through the following initialization sequence:
|
|
@@ -385,6 +485,33 @@ The `once: true` option ensures the fragment is only generated once, preventing
|
|
|
385
485
|
|
|
386
486
|
---
|
|
387
487
|
|
|
488
|
+
## Html.raw
|
|
489
|
+
|
|
490
|
+
Mark a string as trusted HTML that should not be escaped. Use this when you have HTML from a trusted source that you need to render directly.
|
|
491
|
+
|
|
492
|
+
**Warning:** Only use with trusted content. Never use with user-provided input as it bypasses XSS protection.
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
render(Html) {
|
|
496
|
+
const trustedHtml = '<strong>Bold</strong> and <em>italic</em>';
|
|
497
|
+
Html`<div>${Html.raw(trustedHtml)}</div>`;
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
Output:
|
|
502
|
+
|
|
503
|
+
```html
|
|
504
|
+
<div><strong>Bold</strong> and <em>italic</em></div>
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
Without `Html.raw()`, the HTML would be escaped:
|
|
508
|
+
|
|
509
|
+
```html
|
|
510
|
+
<div><strong>Bold</strong> and <em>italic</em></div>
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
388
515
|
## setup
|
|
389
516
|
|
|
390
517
|
The `setup` function wires up an external data-source. This is done with the `attachStore` argument that binds a data source to your renderer.
|
|
@@ -470,6 +597,7 @@ Available properties and methods on `this`:
|
|
|
470
597
|
| `this.wrappedContent` | Text content between your tags. `<my-elem>Hi!</my-elem>` = `"Hi!"` |
|
|
471
598
|
| `this.element` | Reference to your created DOM element |
|
|
472
599
|
| `this.dataset` | Read/write access to all `data-*` attributes |
|
|
600
|
+
| `this.innerShadow` | Get the innerHTML of the element's rendered content |
|
|
473
601
|
|
|
474
602
|
### this.attrs
|
|
475
603
|
|
|
@@ -856,6 +984,472 @@ customElements.define(
|
|
|
856
984
|
|
|
857
985
|
---
|
|
858
986
|
|
|
987
|
+
# Signals
|
|
988
|
+
|
|
989
|
+
hyper-element includes a built-in signals API for fine-grained reactivity, similar to Solid.js or Preact Signals. Signals provide automatic dependency tracking and efficient updates.
|
|
990
|
+
|
|
991
|
+
```js
|
|
992
|
+
import { signal, computed, effect, batch, untracked } from 'hyper-element';
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
## signal
|
|
996
|
+
|
|
997
|
+
Creates a reactive signal that holds a value and notifies subscribers when it changes.
|
|
998
|
+
|
|
999
|
+
```js
|
|
1000
|
+
const count = signal(0);
|
|
1001
|
+
|
|
1002
|
+
// Read value (tracks dependencies in effects/computed)
|
|
1003
|
+
console.log(count.value); // 0
|
|
1004
|
+
|
|
1005
|
+
// Write value (notifies subscribers)
|
|
1006
|
+
count.value = 1;
|
|
1007
|
+
|
|
1008
|
+
// Read without tracking
|
|
1009
|
+
count.peek(); // 1
|
|
1010
|
+
|
|
1011
|
+
// Subscribe to changes
|
|
1012
|
+
const unsubscribe = count.subscribe(() => {
|
|
1013
|
+
console.log('Count changed:', count.peek());
|
|
1014
|
+
});
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
## computed
|
|
1018
|
+
|
|
1019
|
+
Creates a derived signal that automatically recomputes when its dependencies change. Computation is lazy and cached.
|
|
1020
|
+
|
|
1021
|
+
```js
|
|
1022
|
+
const count = signal(0);
|
|
1023
|
+
const doubled = computed(() => count.value * 2);
|
|
1024
|
+
|
|
1025
|
+
console.log(doubled.value); // 0
|
|
1026
|
+
|
|
1027
|
+
count.value = 5;
|
|
1028
|
+
console.log(doubled.value); // 10
|
|
1029
|
+
|
|
1030
|
+
// Read without tracking
|
|
1031
|
+
doubled.peek(); // 10
|
|
1032
|
+
```
|
|
1033
|
+
|
|
1034
|
+
## effect
|
|
1035
|
+
|
|
1036
|
+
Creates a side effect that runs immediately and re-runs whenever its dependencies change. Can return a cleanup function.
|
|
1037
|
+
|
|
1038
|
+
```js
|
|
1039
|
+
const count = signal(0);
|
|
1040
|
+
|
|
1041
|
+
// Effect runs immediately, then on every change
|
|
1042
|
+
const cleanup = effect(() => {
|
|
1043
|
+
console.log('Count is:', count.value);
|
|
1044
|
+
|
|
1045
|
+
// Optional cleanup function
|
|
1046
|
+
return () => {
|
|
1047
|
+
console.log('Cleaning up previous effect');
|
|
1048
|
+
};
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
count.value = 1;
|
|
1052
|
+
// Logs: "Cleaning up previous effect"
|
|
1053
|
+
// Logs: "Count is: 1"
|
|
1054
|
+
|
|
1055
|
+
// Stop the effect
|
|
1056
|
+
cleanup();
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
## batch
|
|
1060
|
+
|
|
1061
|
+
Batches multiple signal updates so effects only run once after all updates complete.
|
|
1062
|
+
|
|
1063
|
+
```js
|
|
1064
|
+
const firstName = signal('John');
|
|
1065
|
+
const lastName = signal('Doe');
|
|
1066
|
+
|
|
1067
|
+
effect(() => {
|
|
1068
|
+
console.log(`${firstName.value} ${lastName.value}`);
|
|
1069
|
+
});
|
|
1070
|
+
// Logs: "John Doe"
|
|
1071
|
+
|
|
1072
|
+
// Without batch: effect would run twice
|
|
1073
|
+
// With batch: effect runs once after both updates
|
|
1074
|
+
batch(() => {
|
|
1075
|
+
firstName.value = 'Jane';
|
|
1076
|
+
lastName.value = 'Smith';
|
|
1077
|
+
});
|
|
1078
|
+
// Logs: "Jane Smith" (only once)
|
|
1079
|
+
```
|
|
1080
|
+
|
|
1081
|
+
## untracked
|
|
1082
|
+
|
|
1083
|
+
Reads signals without creating dependencies. Useful for reading values in effects without subscribing to changes.
|
|
1084
|
+
|
|
1085
|
+
```js
|
|
1086
|
+
const count = signal(0);
|
|
1087
|
+
const other = signal('hello');
|
|
1088
|
+
|
|
1089
|
+
effect(() => {
|
|
1090
|
+
// This dependency IS tracked
|
|
1091
|
+
console.log('Count:', count.value);
|
|
1092
|
+
|
|
1093
|
+
// This read is NOT tracked - effect won't re-run when 'other' changes
|
|
1094
|
+
const otherValue = untracked(() => other.value);
|
|
1095
|
+
console.log('Other:', otherValue);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
count.value = 1; // Effect re-runs
|
|
1099
|
+
other.value = 'world'; // Effect does NOT re-run
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
## Using Signals with hyper-element
|
|
1103
|
+
|
|
1104
|
+
Signals integrate naturally with hyper-element's setup/render lifecycle:
|
|
1105
|
+
|
|
1106
|
+
```js
|
|
1107
|
+
import hyperElement, { signal, effect } from 'hyper-element';
|
|
1108
|
+
|
|
1109
|
+
hyperElement('counter-app', {
|
|
1110
|
+
setup: (ctx, onNext) => {
|
|
1111
|
+
const count = signal(0);
|
|
1112
|
+
|
|
1113
|
+
// Trigger re-render when count changes
|
|
1114
|
+
const stopEffect = effect(() => {
|
|
1115
|
+
onNext(() => ({ count: count.value }))();
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// Expose increment method
|
|
1119
|
+
ctx.increment = () => count.value++;
|
|
1120
|
+
|
|
1121
|
+
// Cleanup effect on disconnect
|
|
1122
|
+
return stopEffect;
|
|
1123
|
+
},
|
|
1124
|
+
|
|
1125
|
+
handleClick: (ctx) => ctx.increment(),
|
|
1126
|
+
|
|
1127
|
+
render: (Html, ctx, store) => Html`
|
|
1128
|
+
<button onclick=${ctx.handleClick}>
|
|
1129
|
+
Count: ${store?.count ?? 0}
|
|
1130
|
+
</button>
|
|
1131
|
+
`,
|
|
1132
|
+
});
|
|
1133
|
+
```
|
|
1134
|
+
|
|
1135
|
+
---
|
|
1136
|
+
|
|
1137
|
+
# Server-Side Rendering (SSR)
|
|
1138
|
+
|
|
1139
|
+
hyper-element supports server-side rendering for faster initial page loads and SEO. The SSR system has two parts:
|
|
1140
|
+
|
|
1141
|
+
1. **Server-side API** - Render components to HTML strings in Node.js/Deno/Bun
|
|
1142
|
+
2. **Client-side hydration** - Capture user interactions during page load and replay them after components register
|
|
1143
|
+
|
|
1144
|
+
## Server-Side API
|
|
1145
|
+
|
|
1146
|
+
Import SSR functions from the dedicated server entry point:
|
|
1147
|
+
|
|
1148
|
+
```js
|
|
1149
|
+
// Node.js / Bun / Deno
|
|
1150
|
+
import {
|
|
1151
|
+
renderElement,
|
|
1152
|
+
renderElements,
|
|
1153
|
+
createRenderer,
|
|
1154
|
+
ssrHtml,
|
|
1155
|
+
escapeHtml,
|
|
1156
|
+
safeHtml,
|
|
1157
|
+
} from 'hyper-element/ssr/server';
|
|
1158
|
+
```
|
|
1159
|
+
|
|
1160
|
+
### renderElement
|
|
1161
|
+
|
|
1162
|
+
Render a single component to an HTML string:
|
|
1163
|
+
|
|
1164
|
+
```js
|
|
1165
|
+
const html = await renderElement('user-card', {
|
|
1166
|
+
attrs: { name: 'Alice', role: 'Admin' },
|
|
1167
|
+
store: { lastLogin: '2024-01-15' },
|
|
1168
|
+
render: (Html, ctx) => Html`
|
|
1169
|
+
<div class="card">
|
|
1170
|
+
<h2>${ctx.attrs.name}</h2>
|
|
1171
|
+
<span>${ctx.attrs.role}</span>
|
|
1172
|
+
<small>Last login: ${ctx.store.lastLogin}</small>
|
|
1173
|
+
</div>
|
|
1174
|
+
`,
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// Result: <user-card name="Alice" role="Admin"><div class="card">...</div></user-card>
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
**Options:**
|
|
1181
|
+
|
|
1182
|
+
| Option | Type | Description |
|
|
1183
|
+
| ----------- | ---------- | ----------------------------------------------------- |
|
|
1184
|
+
| `attrs` | `object` | Attributes to pass to the component |
|
|
1185
|
+
| `store` | `object` | Store data available in render |
|
|
1186
|
+
| `render` | `function` | Required render function `(Html, ctx) => Html\`...\`` |
|
|
1187
|
+
| `shadowDOM` | `boolean` | Wrap output in Declarative Shadow DOM template |
|
|
1188
|
+
| `fragments` | `object` | Fragment functions for async content |
|
|
1189
|
+
|
|
1190
|
+
### createRenderer
|
|
1191
|
+
|
|
1192
|
+
Create a reusable renderer for a component:
|
|
1193
|
+
|
|
1194
|
+
```js
|
|
1195
|
+
const renderUserCard = createRenderer(
|
|
1196
|
+
'user-card',
|
|
1197
|
+
(Html, ctx) => Html`
|
|
1198
|
+
<div class="card">
|
|
1199
|
+
<h2>${ctx.attrs.name}</h2>
|
|
1200
|
+
</div>
|
|
1201
|
+
`,
|
|
1202
|
+
{ shadowDOM: false } // default options
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
// Use it multiple times
|
|
1206
|
+
const html1 = await renderUserCard({ name: 'Alice' });
|
|
1207
|
+
const html2 = await renderUserCard({ name: 'Bob' });
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
### renderElements
|
|
1211
|
+
|
|
1212
|
+
Render multiple components in parallel:
|
|
1213
|
+
|
|
1214
|
+
```js
|
|
1215
|
+
const results = await renderElements([
|
|
1216
|
+
{ tagName: 'user-card', attrs: { name: 'Alice' }, render: renderFn },
|
|
1217
|
+
{ tagName: 'user-card', attrs: { name: 'Bob' }, render: renderFn },
|
|
1218
|
+
]);
|
|
1219
|
+
// Returns array of HTML strings
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
### ssrHtml
|
|
1223
|
+
|
|
1224
|
+
Tagged template literal for rendering HTML strings directly. SVG content is auto-detected when using `<svg>` tags:
|
|
1225
|
+
|
|
1226
|
+
```js
|
|
1227
|
+
const header = ssrHtml`<header><h1>${title}</h1></header>`;
|
|
1228
|
+
const icon = ssrHtml`<svg viewBox="0 0 24 24"><path d="${pathData}"/></svg>`;
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
### escapeHtml / safeHtml
|
|
1232
|
+
|
|
1233
|
+
Utility functions for HTML escaping:
|
|
1234
|
+
|
|
1235
|
+
```js
|
|
1236
|
+
// Escape user input
|
|
1237
|
+
const safe = escapeHtml('<script>alert("xss")</script>');
|
|
1238
|
+
// Result: <script>alert("xss")</script>
|
|
1239
|
+
|
|
1240
|
+
// Mark trusted HTML as safe (bypasses escaping)
|
|
1241
|
+
const trusted = safeHtml('<strong>Bold</strong>');
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
### Fragments in SSR
|
|
1245
|
+
|
|
1246
|
+
Fragments work on the server too for async content:
|
|
1247
|
+
|
|
1248
|
+
```js
|
|
1249
|
+
const html = await renderElement('user-profile', {
|
|
1250
|
+
attrs: { userId: '123' },
|
|
1251
|
+
fragments: {
|
|
1252
|
+
FriendCount: async (userId) => {
|
|
1253
|
+
const count = await fetchFriendCount(userId);
|
|
1254
|
+
return { text: `${count} friends` };
|
|
1255
|
+
},
|
|
1256
|
+
},
|
|
1257
|
+
render: (Html, ctx) => Html`
|
|
1258
|
+
<div>
|
|
1259
|
+
<h1>Profile</h1>
|
|
1260
|
+
<p>${{ FriendCount: ctx.attrs.userId }}</p>
|
|
1261
|
+
</div>
|
|
1262
|
+
`,
|
|
1263
|
+
});
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
## Client-Side Hydration
|
|
1269
|
+
|
|
1270
|
+
When SSR HTML arrives in the browser, users can interact with elements before JavaScript loads and components register. hyper-element captures these interactions and replays them after hydration.
|
|
1271
|
+
|
|
1272
|
+
### How It Works
|
|
1273
|
+
|
|
1274
|
+
```
|
|
1275
|
+
1. CAPTURE - hyper-element loads in <head>, starts listening for events
|
|
1276
|
+
2. BUFFER - User interacts with SSR markup, events are stored
|
|
1277
|
+
3. REPLAY - After customElements.define() + first render, events replay
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
### configureSSR
|
|
1281
|
+
|
|
1282
|
+
Configure which events to capture (call before components register):
|
|
1283
|
+
|
|
1284
|
+
```js
|
|
1285
|
+
import { configureSSR } from 'hyper-element';
|
|
1286
|
+
|
|
1287
|
+
configureSSR({
|
|
1288
|
+
events: ['click', 'input', 'change', 'submit'], // Events to capture
|
|
1289
|
+
devMode: true, // Show visual indicator during capture (dev only)
|
|
1290
|
+
});
|
|
1291
|
+
```
|
|
1292
|
+
|
|
1293
|
+
**Default captured events:** `click`, `dblclick`, `input`, `change`, `submit`, `keydown`, `keyup`, `keypress`, `focus`, `blur`, `focusin`, `focusout`, `touchstart`, `touchend`, `touchmove`, `touchcancel`
|
|
1294
|
+
|
|
1295
|
+
### Lifecycle Hooks
|
|
1296
|
+
|
|
1297
|
+
Components can hook into the hydration process:
|
|
1298
|
+
|
|
1299
|
+
```js
|
|
1300
|
+
customElements.define(
|
|
1301
|
+
'my-component',
|
|
1302
|
+
class extends hyperElement {
|
|
1303
|
+
// Called before events are replayed
|
|
1304
|
+
// Return filtered/modified events array
|
|
1305
|
+
onBeforeHydrate(bufferedEvents) {
|
|
1306
|
+
console.log('Events captured:', bufferedEvents.length);
|
|
1307
|
+
// Filter out old events
|
|
1308
|
+
return bufferedEvents.filter((e) => Date.now() - e.timestamp < 5000);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Called after all events have been replayed
|
|
1312
|
+
onAfterHydrate() {
|
|
1313
|
+
console.log('Hydration complete!');
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
render(Html) {
|
|
1317
|
+
Html`<button>Click me</button>`;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
);
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
### BufferedEvent Structure
|
|
1324
|
+
|
|
1325
|
+
Each captured event contains:
|
|
1326
|
+
|
|
1327
|
+
```ts
|
|
1328
|
+
interface BufferedEvent {
|
|
1329
|
+
type: string; // 'click', 'input', etc.
|
|
1330
|
+
timestamp: number; // When event occurred
|
|
1331
|
+
targetPath: string; // DOM path like 'DIV:0/BUTTON:1'
|
|
1332
|
+
detail: object; // Event-specific properties
|
|
1333
|
+
}
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
### State Preservation
|
|
1337
|
+
|
|
1338
|
+
The hydration system automatically preserves:
|
|
1339
|
+
|
|
1340
|
+
- **Form values** - Input, textarea, select values via `input` events
|
|
1341
|
+
- **Checkbox/radio state** - Checked state captured and restored
|
|
1342
|
+
- **Scroll position** - Scroll positions within components
|
|
1343
|
+
|
|
1344
|
+
---
|
|
1345
|
+
|
|
1346
|
+
## SSR Configuration
|
|
1347
|
+
|
|
1348
|
+
### Full Configuration Reference
|
|
1349
|
+
|
|
1350
|
+
```js
|
|
1351
|
+
import { configureSSR } from 'hyper-element';
|
|
1352
|
+
|
|
1353
|
+
configureSSR({
|
|
1354
|
+
// Events to capture during SSR hydration
|
|
1355
|
+
events: [
|
|
1356
|
+
'click',
|
|
1357
|
+
'dblclick',
|
|
1358
|
+
'input',
|
|
1359
|
+
'change',
|
|
1360
|
+
'submit',
|
|
1361
|
+
'keydown',
|
|
1362
|
+
'keyup',
|
|
1363
|
+
'keypress',
|
|
1364
|
+
'focus',
|
|
1365
|
+
'blur',
|
|
1366
|
+
'focusin',
|
|
1367
|
+
'focusout',
|
|
1368
|
+
'touchstart',
|
|
1369
|
+
'touchend',
|
|
1370
|
+
'touchmove',
|
|
1371
|
+
'touchcancel',
|
|
1372
|
+
],
|
|
1373
|
+
|
|
1374
|
+
// Show orange "SSR Capture Active" badge (development only)
|
|
1375
|
+
devMode: false,
|
|
1376
|
+
});
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
---
|
|
1380
|
+
|
|
1381
|
+
## Complete SSR Example
|
|
1382
|
+
|
|
1383
|
+
**Server (Node.js):**
|
|
1384
|
+
|
|
1385
|
+
```js
|
|
1386
|
+
import { renderElement } from 'hyper-element/ssr/server';
|
|
1387
|
+
|
|
1388
|
+
const html = await renderElement('todo-list', {
|
|
1389
|
+
attrs: { title: 'My Tasks' },
|
|
1390
|
+
store: {
|
|
1391
|
+
items: [
|
|
1392
|
+
{ id: 1, text: 'Learn SSR', done: false },
|
|
1393
|
+
{ id: 2, text: 'Build app', done: false },
|
|
1394
|
+
],
|
|
1395
|
+
},
|
|
1396
|
+
render: (Html, ctx) => Html`
|
|
1397
|
+
<h1>${ctx.attrs.title}</h1>
|
|
1398
|
+
<ul>
|
|
1399
|
+
{+each ${ctx.store.items}}
|
|
1400
|
+
<li data-id="{id}">{text}</li>
|
|
1401
|
+
{-each}
|
|
1402
|
+
</ul>
|
|
1403
|
+
`,
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
// Serve full HTML page
|
|
1407
|
+
res.send(`
|
|
1408
|
+
<!DOCTYPE html>
|
|
1409
|
+
<html>
|
|
1410
|
+
<head>
|
|
1411
|
+
<script src="/hyper-element.min.js"></script>
|
|
1412
|
+
</head>
|
|
1413
|
+
<body>
|
|
1414
|
+
${html}
|
|
1415
|
+
<script src="/app.js"></script>
|
|
1416
|
+
</body>
|
|
1417
|
+
</html>
|
|
1418
|
+
`);
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
**Client (app.js):**
|
|
1422
|
+
|
|
1423
|
+
```js
|
|
1424
|
+
import hyperElement, { configureSSR } from 'hyper-element';
|
|
1425
|
+
|
|
1426
|
+
// Optional: configure before components register
|
|
1427
|
+
configureSSR({ devMode: true });
|
|
1428
|
+
|
|
1429
|
+
// Register the component - hydration happens automatically
|
|
1430
|
+
hyperElement('todo-list', {
|
|
1431
|
+
onBeforeHydrate(events) {
|
|
1432
|
+
console.log('Replaying', events.length, 'events');
|
|
1433
|
+
return events;
|
|
1434
|
+
},
|
|
1435
|
+
|
|
1436
|
+
onAfterHydrate() {
|
|
1437
|
+
console.log('Todo list hydrated!');
|
|
1438
|
+
},
|
|
1439
|
+
|
|
1440
|
+
render: (Html, ctx, store) => Html`
|
|
1441
|
+
<h1>${ctx.attrs.title}</h1>
|
|
1442
|
+
<ul>
|
|
1443
|
+
{+each ${store.items}}
|
|
1444
|
+
<li data-id="{id}">{text}</li>
|
|
1445
|
+
{-each}
|
|
1446
|
+
</ul>
|
|
1447
|
+
`,
|
|
1448
|
+
});
|
|
1449
|
+
```
|
|
1450
|
+
|
|
1451
|
+
---
|
|
1452
|
+
|
|
859
1453
|
# Best Practices
|
|
860
1454
|
|
|
861
1455
|
## Always Use Html.wire for Lists
|
|
@@ -945,12 +1539,15 @@ setup(attachStore) {
|
|
|
945
1539
|
```
|
|
946
1540
|
hyper-element/
|
|
947
1541
|
├── src/ # Source files (ES modules)
|
|
948
|
-
│ ├── core/ # Core utilities
|
|
949
1542
|
│ ├── attributes/ # Attribute handling
|
|
950
|
-
│ ├──
|
|
951
|
-
│ ├── html/ # HTML
|
|
1543
|
+
│ ├── core/ # Core utilities
|
|
1544
|
+
│ ├── html/ # HTML tag functions
|
|
952
1545
|
│ ├── lifecycle/ # Lifecycle hooks
|
|
953
|
-
│
|
|
1546
|
+
│ ├── render/ # Custom render core (uhtml-inspired)
|
|
1547
|
+
│ ├── signals/ # Reactive primitives (signal, computed, effect)
|
|
1548
|
+
│ ├── template/ # Template processing
|
|
1549
|
+
│ ├── utils/ # Shared utilities
|
|
1550
|
+
│ └── index.js # Main export
|
|
954
1551
|
├── build/
|
|
955
1552
|
│ ├── hyperElement.min.js # Minified production build
|
|
956
1553
|
│ └── hyperElement.min.js.map
|
|
@@ -1015,7 +1612,55 @@ npm run format:fix
|
|
|
1015
1612
|
|
|
1016
1613
|
## Testing
|
|
1017
1614
|
|
|
1018
|
-
|
|
1615
|
+
hyper-element uses a **two-phase test workflow** to ensure both source quality and build integrity:
|
|
1616
|
+
|
|
1617
|
+
### Phase 1: Source Coverage
|
|
1618
|
+
|
|
1619
|
+
```bash
|
|
1620
|
+
npm run test:src
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
- Loads `src/` directly via ES modules + import maps
|
|
1624
|
+
- Collects V8 coverage on source files
|
|
1625
|
+
- Generates HTML report at `coverage/index.html`
|
|
1626
|
+
- Runs SSR tests with coverage
|
|
1627
|
+
- **Requires 100% coverage** on all metrics
|
|
1628
|
+
|
|
1629
|
+
### Phase 2: Bundle Verification
|
|
1630
|
+
|
|
1631
|
+
```bash
|
|
1632
|
+
npm run test:bundle
|
|
1633
|
+
```
|
|
1634
|
+
|
|
1635
|
+
- Loads built `build/hyperElement.min.js`
|
|
1636
|
+
- Verifies nothing broke during bundling
|
|
1637
|
+
- No coverage collected (just verification)
|
|
1638
|
+
|
|
1639
|
+
### Full Test Suite
|
|
1640
|
+
|
|
1641
|
+
```bash
|
|
1642
|
+
npm test
|
|
1643
|
+
```
|
|
1644
|
+
|
|
1645
|
+
Runs both phases sequentially: source coverage first, then bundle verification.
|
|
1646
|
+
|
|
1647
|
+
### Viewing Coverage Report
|
|
1648
|
+
|
|
1649
|
+
After running tests, open the HTML coverage report:
|
|
1650
|
+
|
|
1651
|
+
```bash
|
|
1652
|
+
open coverage/index.html
|
|
1653
|
+
```
|
|
1654
|
+
|
|
1655
|
+
This shows:
|
|
1656
|
+
|
|
1657
|
+
- File-by-file coverage breakdown
|
|
1658
|
+
- Line-by-line highlighting of covered/uncovered code
|
|
1659
|
+
- Statement, branch, and function metrics
|
|
1660
|
+
|
|
1661
|
+
### Test Files
|
|
1662
|
+
|
|
1663
|
+
Tests are located in `kitchensink/` and run via Playwright. See `kitchensink/kitchensink.spec.js` for the test suite.
|
|
1019
1664
|
|
|
1020
1665
|
## Contributing
|
|
1021
1666
|
|
|
@@ -1025,7 +1670,6 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.
|
|
|
1025
1670
|
|
|
1026
1671
|
[shadow-dom]: https://developers.google.com/web/fundamentals/web-components/shadowdom
|
|
1027
1672
|
[innerHTML]: https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
|
|
1028
|
-
[hyperHTML]: https://viperhtml.js.org/hyper.html
|
|
1029
1673
|
[Custom Elements]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements
|
|
1030
1674
|
[Test system]: https://jsfiddle.net/codemeasandwich/k25e6ufv/36/
|
|
1031
1675
|
[promise]: https://scotch.io/tutorials/javascript-promises-for-dummies#understanding-promises
|