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 CHANGED
@@ -5,9 +5,10 @@
5
5
  [![CI](https://github.com/codemeasandwich/hyper-element/actions/workflows/publish.yml/badge.svg)](https://github.com/codemeasandwich/hyper-element/actions/workflows/publish.yml)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/codemeasandwich/hyper-element)
8
+ [![XSS Protected](https://img.shields.io/badge/XSS-Protected-blue.svg)](https://github.com/codemeasandwich/hyper-element)
8
9
  [![ES6+](https://img.shields.io/badge/ES6+-supported-blue.svg)](https://caniuse.com/es6)
9
10
 
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.
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
- customElements.define(
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
- customElements.define(
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`. hyperHTML is bundled automatically.
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 | 67+ |
69
- | Firefox | 63+ |
70
- | Safari | 10.1+ |
71
- | Edge | 79+ |
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
- - With only 1 dependency: [hyperHTML]
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>&lt;strong&gt;Bold&lt;/strong&gt; and &lt;em&gt;italic&lt;/em&gt;</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: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;
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
- │ ├── template/ # Template processing
951
- │ ├── html/ # HTML rendering
1543
+ │ ├── core/ # Core utilities
1544
+ │ ├── html/ # HTML tag functions
952
1545
  │ ├── lifecycle/ # Lifecycle hooks
953
- └── hyperElement.js # Main export
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
- See [kitchensink/README.md](kitchensink/README.md) for the full testing guide.
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