hyper-element 1.1.0 → 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 +560 -21
- package/build/hyperElement.min.js +14 -1
- package/build/hyperElement.min.js.map +3 -3
- package/index.d.ts +344 -3
- package/package.json +13 -8
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
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.
|
|
@@ -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)
|
|
@@ -65,10 +52,10 @@ 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
|
|
|
@@ -81,6 +68,7 @@ For older browsers, a [Custom Elements polyfill](https://github.com/webcomponent
|
|
|
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/)
|
|
@@ -122,6 +110,16 @@ For older browsers, a [Custom Elements polyfill](https://github.com/webcomponent
|
|
|
122
110
|
- [Backbone](#backbone)
|
|
123
111
|
- [MobX](#mobx)
|
|
124
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)
|
|
125
123
|
- [Best Practices](#best-practices)
|
|
126
124
|
- [Development](#development)
|
|
127
125
|
|
|
@@ -170,7 +168,6 @@ In addition to class-based components, hyper-element supports a functional API t
|
|
|
170
168
|
```js
|
|
171
169
|
// 1. Full definition with tag (auto-registers)
|
|
172
170
|
hyperElement('my-counter', {
|
|
173
|
-
observedAttributes: ['count'],
|
|
174
171
|
setup: (ctx, onNext) => {
|
|
175
172
|
/* ... */
|
|
176
173
|
},
|
|
@@ -488,6 +485,33 @@ The `once: true` option ensures the fragment is only generated once, preventing
|
|
|
488
485
|
|
|
489
486
|
---
|
|
490
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
|
+
|
|
491
515
|
## setup
|
|
492
516
|
|
|
493
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.
|
|
@@ -573,6 +597,7 @@ Available properties and methods on `this`:
|
|
|
573
597
|
| `this.wrappedContent` | Text content between your tags. `<my-elem>Hi!</my-elem>` = `"Hi!"` |
|
|
574
598
|
| `this.element` | Reference to your created DOM element |
|
|
575
599
|
| `this.dataset` | Read/write access to all `data-*` attributes |
|
|
600
|
+
| `this.innerShadow` | Get the innerHTML of the element's rendered content |
|
|
576
601
|
|
|
577
602
|
### this.attrs
|
|
578
603
|
|
|
@@ -959,6 +984,472 @@ customElements.define(
|
|
|
959
984
|
|
|
960
985
|
---
|
|
961
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
|
+
|
|
962
1453
|
# Best Practices
|
|
963
1454
|
|
|
964
1455
|
## Always Use Html.wire for Lists
|
|
@@ -1121,6 +1612,54 @@ npm run format:fix
|
|
|
1121
1612
|
|
|
1122
1613
|
## Testing
|
|
1123
1614
|
|
|
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
|
+
|
|
1124
1663
|
Tests are located in `kitchensink/` and run via Playwright. See `kitchensink/kitchensink.spec.js` for the test suite.
|
|
1125
1664
|
|
|
1126
1665
|
## Contributing
|