hyper-element 0.10.0 → 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 CHANGED
@@ -1,639 +1,1033 @@
1
1
  # hyper-element
2
2
 
3
- Combining the best of [hyperHTML] and [Custom Elements]!
3
+ [![npm version](https://img.shields.io/npm/v/hyper-element.svg)](https://www.npmjs.com/package/hyper-element)
4
+ [![npm package size](https://img.shields.io/bundlephobia/minzip/hyper-element)](https://bundlephobia.com/package/hyper-element)
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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/codemeasandwich/hyper-element)
8
+ [![ES6+](https://img.shields.io/badge/ES6+-supported-blue.svg)](https://caniuse.com/es6)
4
9
 
5
- [![npm version](https://badge.fury.io/js/hyper-element.svg)](https://www.npmjs.com/package/hyper-element)
6
- [![CDN link](https://img.shields.io/badge/CDN_link-hyper--element-red.svg)](https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js)
7
- [![gzip size](http://img.badgesize.io/https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js?compression=gzip)](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
- Your new custom-element will be rendered with the super fast [hyperHTML] and will react to tag attribute and store changes.
11
-
12
- ## why hyper-element
14
+ # Installation
13
15
 
16
+ ## npm
14
17
 
15
- * hyper-element is fast & small
16
- * With only 1 dependency: [hyperHTML]
17
- * With a completely stateless approach, setting and reseting the view is trivial
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
- # [Live Demo](https://jsfiddle.net/codemeasandwich/k25e6ufv/)
22
+ ### ES6 Modules
25
23
 
26
- ### If you like it, please [★ it on github](https://github.com/codemeasandwich/hyper-element)
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
- - [Define a Custom Element](#define-a-custom-element)
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
- # Define a custom-element
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
- ```js
55
- document.registerElement("my-elem", class extends hyperElement{
52
+ ## CDN (Browser)
56
53
 
57
- render(Html){
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
- })// END my-elem
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
- If using **webpack**
61
+ The `hyperElement` class will be available globally on `window.hyperElement`.
65
62
 
66
- ```
67
- const hyperElement from "hyper-element"
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.
75
+
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
+ ---
69
129
 
70
- To use your element in brower
130
+ # Define a custom-element
71
131
 
72
132
  ```html
73
133
  <!DOCTYPE html>
74
134
  <html>
75
- <head>
76
- <script src="https://cdn.jsdelivr.net/npm/webcomponentsjs@latest/lite.min.js"></script>
77
- <script src="https://cdn.jsdelivr.net/npm/hyperhtml@latest/index.js"></script>
78
- <script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/source/bundle.min.js"></script>
79
- </head>
80
- <body>
81
- <my-elem who="world"></my-elem>
82
- </body>
83
- <html>
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
- # Api
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 *required* and `setup` is *optional*
189
+ There are 2 functions. `render` is _required_ and `setup` is _optional_
190
+
191
+ ## render
192
+
193
+ This is what will be displayed within your element. Use the `Html` to define your content.
194
+
195
+ ```js
196
+ render(Html, store) {
197
+ Html`
198
+ <h1>
199
+ Last updated at ${new Date().toLocaleTimeString()}
200
+ </h1>
201
+ `;
202
+ }
203
+ ```
99
204
 
100
- ### render
205
+ The second argument `store` contains the value returned from your store function (if using `setup()`).
101
206
 
102
- This is what will be displayed with in your element. Use the `Html` to define your content
207
+ ---
103
208
 
104
- #### Html
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
- Html`
112
- <h1>
113
- Lasted updated at ${new Date().toLocaleTimeString()}
114
- </h1>
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
- #### Html.wire
227
+ ## Html.wire
124
228
 
125
- The `.wire` is for creating reusable sub-element
229
+ Create reusable sub-elements with object/id binding for efficient rendering.
126
230
 
127
- The wire can take two arguments `Html.wire(obj,id)`
128
- 1. a refrive object to match with the create node. Allowing for reuse of the exiting node.
129
- 2. a string to identify the markup used. Allowing the markup template to be generated only once.
231
+ The wire takes two arguments `Html.wire(obj, id)`:
130
232
 
131
- Example of displaying a list of users from an array
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
235
+
236
+ ### Example: Rendering a List
132
237
 
133
238
  ```js
134
- Html`
135
- <ul>
136
- ${users.map(user => Html.wire(user,":user_list_item")`<li>${user.name}</li>`)}
137
- </ul>
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
- An **anti-pattern** is to inline the markup as a string
246
+ ### Anti-pattern: Inlining Markup as Strings
142
247
 
143
- BAD example:
248
+ **BAD example:**
144
249
 
145
250
  ```js
146
- Html`
147
- <ul>
148
- ${users.map(user => `<li>${user.name}</li>`)}
149
- </ul>
150
- `
251
+ Html`
252
+ <ul>
253
+ ${users.map((user) => `<li>${user.name}</li>`)}
254
+ </ul>
255
+ `;
151
256
  ```
152
257
 
153
- This will create a new node for every element on every render. The is have a **Negative impact on performance** and output will **Not be sanitized**. So DONT do 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
- #### Html.lite
263
+ ### Block Syntax
158
264
 
159
- The `.lite` is for creating once off sub-element
265
+ The Html function supports block syntax for iteration and conditionals directly in tagged template literals:
160
266
 
161
- Example of wrapping the [jQuary date picker](https://jqueryui.com/datepicker/)
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
- ```js
274
+ #### {+each} - Iteration
164
275
 
165
- onSelect(dateText, inst){
166
- console.log("selected time "+dateText)
167
- } // END onSelect
276
+ For cleaner list rendering, use the `{+each}...{-each}` syntax:
168
277
 
169
- Date(lite){
170
- const inputElem = lite`<input type="text"/>`
171
- $(inputElem).datepicker({onSelect:this.onSelect});
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
- render(Html){
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
- ### setup
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
- The `setup` function wires up an external data-source. This is done with the `setupStore` argument that binds a data source to your renderer.
299
+ **Formatting rules for `{...}` output:**
190
300
 
191
- #### Connect a data source
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
- Example of re-rendering when the mouse moves. Will pass mouse values to render
308
+ **Examples:**
194
309
 
195
310
  ```js
196
- // getMouseValues(){ ... }
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
- setup(setupStore){
332
+ #### {+if} - Conditionals
199
333
 
200
- // the getMouseValues function will be call before each render and pass to render
201
- const callMeOnStoreChange = setupStore(getMouseValues)
334
+ Render content based on a condition:
202
335
 
203
- // call next on every mouse event
204
- onMouseMove(callMeOnStoreChange)
336
+ ```js
337
+ Html`{+if ${isLoggedIn}}<p>Welcome back!</p>{-if}`;
205
338
 
206
- // cleanup logic
207
- return ()=> console.warn("On remove, do component cleanup here")
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
- #### re-rendering without a data source
343
+ #### {+unless} - Negated Conditionals
212
344
 
213
- Example of re-rendering every second
345
+ Render content when condition is falsy (opposite of {+if}):
214
346
 
215
347
  ```js
216
- setup(setupStore){
217
- setInterval(setupStore(), 1000);
218
- }// END setup
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
- #### Set initial values to pass to every render
354
+ ---
355
+
356
+ ## Html.lite
222
357
 
223
- Example of hard coding an object that will be used on **every** render
358
+ Create once-off sub-elements for integrating external libraries.
359
+
360
+ ### Example: Wrapping jQuery DatePicker
224
361
 
225
362
  ```js
226
- setup(setupStore){
227
- setupStore({max_levels:3})
228
- }// END setup
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
- #### How to cleanup
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
- Any logic you wish to run when the **element** is removed from the page should be returned as a function from the `setup` function
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
- // listen to a WebSocket
237
- setup(setupStore){
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
- let newSocketValue;
240
- const callMeOnStoreChange = setupStore(()=> newSocketValue);
241
- const ws = new WebSocket("ws://127.0.0.1/data");
399
+ // call onStoreChange on every mouse event
400
+ onMouseMove(onStoreChange);
242
401
 
243
- ws.onmessage = ({data}) => {
244
- newSocketValue = JSON.parse(data);
245
- callMeOnStoreChange()
246
- }
402
+ // cleanup logic
403
+ return () => console.warn('On remove, do component cleanup here');
404
+ }
405
+ ```
247
406
 
248
- // Return way to unsubscribe
249
- return ws.close.bind(ws)
250
- }// END setup
407
+ **Live Example of [attach a store](https://codepen.io/codemeasandwich/pen/VOQWeN)**
251
408
 
252
- render(Html,incomingMessage){
253
- // ...
254
- }// END render
409
+ ### Re-rendering Without a Data Source
410
+
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
- Returning a "teardown function" from `setup` address's the problem of needing a reference to the resource you want to release.
419
+ ### Set Initial Values
258
420
 
259
- > If the "teardown function" was a public function. We would need to store the reference to the resource somewhere. So the teardown can access it when needed.
421
+ Pass static data to every render:
260
422
 
261
- With this approach there is no leaking of references.
423
+ ```js
424
+ setup(attachStore) {
425
+ attachStore({ max_levels: 3 }); // passed to every render
426
+ }
427
+ ```
428
+
429
+ ### Cleanup on Removal
262
430
 
263
- #### To subscribe to 2 events
431
+ Return a function from `setup` to run cleanup when the element is removed from the DOM:
264
432
 
265
433
  ```js
266
- setup(setupStore){
434
+ setup(attachStore) {
435
+ let newSocketValue;
436
+ const onStoreChange = attachStore(() => newSocketValue);
437
+ const ws = new WebSocket('ws://127.0.0.1/data');
438
+
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
+ ```
267
448
 
268
- const callMeOnStoreChange = setupStore(user);
449
+ ### Multiple Subscriptions
269
450
 
270
- mobx.autorun(callMeOnStoreChange); // update when changed (real-time feedback)
271
- setInterval(callMeOnStoreChange, 1000); // update every second (update "the time is now ...")
451
+ You can trigger re-renders from multiple sources:
272
452
 
273
- }// END setup
453
+ ```js
454
+ setup(attachStore) {
455
+ const onStoreChange = attachStore(user);
274
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
- ### this
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
- ## Advanced attributes
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
- ### Dynamic attributes with custom-element children
476
+ ### this.attrs
300
477
 
301
- Being able to set attributes at run-time should be the same for dealing with a native element and ones defined by hyper-element.
478
+ Attributes are automatically type-coerced:
302
479
 
303
- **⚠ To support dynamic attributes on custom elements YOU MUST USE `customElements.define` which requires native ES6 support! Use the native source `/source/hyperElement.js` NOT `/source/bundle.js`**
480
+ | Input | Output | Type |
481
+ | --------- | --------- | ------ |
482
+ | `"42"` | `42` | Number |
483
+ | `"3.14"` | `3.14` | Number |
484
+ | `"hello"` | `"hello"` | String |
304
485
 
305
- 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).
486
+ ### this.dataset
306
487
 
307
- **Example:**
488
+ The dataset provides proxied access to `data-*` attributes with automatic JSON parsing:
308
489
 
309
- In you document:
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
- <script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/source/hyperElement.js"></script>
313
- <users-elem />
501
+ <my-elem data-users='["ann","bob"]'></my-elem>
502
+ ```
503
+
504
+ ```js
505
+ this.dataset.users; // ["ann", "bob"]
314
506
  ```
315
507
 
316
- Implementation:
508
+ The `dataset` is a **live reflection**. Changes update the matching data attribute on the element:
317
509
 
318
510
  ```js
319
- window.customElements.define("a-user",class extends hyperElement{
320
- render(Html){
321
- const onClick = () => this.attrs.hi("Hello from "+this.attrs.name);
322
- Html`${this.attrs.name} <button onclick=${onClick}>Say hi!</button>`
323
- }
324
- })
511
+ this.dataset.user = { name: 'Alice' }; // Updates data-user attribute
512
+ ```
513
+
514
+ ---
515
+
516
+ ## Advanced Attributes
325
517
 
326
- window.customElements.define("users-elem",class extends hyperElement{
327
- onHi(val){
328
- console.log("hi was clicked",val)
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
- render(Html){
331
- Html`<a-user hi=${this.onHi} name="Beckett" />`
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
- Output:
552
+ **Live Example of passing an [onclick to a child element](https://codepen.io/codemeasandwich/pen/rgdvPX)**
337
553
 
338
- ```html
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
- ## Templates
556
+ # Templates
347
557
 
348
- You can declare markup to be used as a template within the custom element
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 an attribute "templates" to your custom element
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
- In you document:
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
- document.registerElement("my-list",class extends hyperElement{
371
-
372
- render(Html){
373
- Html`
374
- ${this.dataset.json.map(user => Html.template(user))}
375
- `
376
- }// END render
377
- })// END my-list
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
- ```Html
382
- <my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]' >
383
- <div>
384
- <a href="">ann</a>
385
- </div>
386
- <div>
387
- <a href="">bob</a>
388
- </div>
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
- ## Fragments
600
+ **Live Example of using [templates](https://codepen.io/codemeasandwich/pen/LoQLrK)**
393
601
 
394
- Fragments are pieces of content that can be loaded *asynchronously*.
602
+ ---
395
603
 
396
- You define one with a class property starting with a **capital letter**.
604
+ ## Basic Template Syntax
397
605
 
398
- To use one within your renderer. Pass an object with a property matching the fragment name and any values needed.
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) |
399
614
 
400
- The fragment function should return an object with the following properties
615
+ ---
401
616
 
402
- * **placeholder:** the placeholder to show while resolving the fragment
403
- * **once:** Only generate the fragment once.
404
- * Default: `false`. The fragment function will be run on every render!
617
+ ## Conditionals: {+if}
405
618
 
406
- and **one** of the following as the fragment's result:
619
+ Show content based on a condition:
407
620
 
408
- * **text:** An escaped string to output
409
- * **any:** An type of content
410
- * **html:** A html string to output, **(Not sanitised)**
411
- * **template:** A [template](#fragment-templates) string to use, **(Is sanitised)**
412
- * **values:** A set of values to be used in the **template**
621
+ ```html
622
+ <status-elem template>{+if active}Online{else}Offline{-if}</status-elem>
623
+ ```
413
624
 
414
- **Example:**
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
+ ```
635
+
636
+ Output: `Online`
637
+
638
+ ---
415
639
 
416
- Implementation:
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
- document.registerElement("my-friends",class extends hyperElement{
649
+ customElements.define(
650
+ 'warning-elem',
651
+ class extends hyperElement {
652
+ render(Html) {
653
+ Html`${Html.template({ valid: false })}`;
654
+ }
655
+ }
656
+ );
657
+ ```
420
658
 
421
- FriendCount(user){
422
- return {
659
+ Output: `Invalid input!`
423
660
 
424
- once:true,
661
+ ---
662
+
663
+ ## Iteration: {+each}
425
664
 
426
- placeholder: "loading your number of friends",
665
+ Loop over arrays:
427
666
 
428
- text:fetch("/user/"+user.userId+"/friends")
429
- .then(b => b.json())
430
- .then(friends => `you have ${friends.count} friends`)
431
- .catch(err => "problem loading friends")
432
- }// END return
433
- }// END FriendCount
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
- render(Html){
436
- const userId = this.attrs.myId
437
- Html`<h2> ${{FriendCount:userId}} </h2>`
438
- }// END render
439
- })// END my-friends
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
- <my-friends myId="1234">
446
- <h2> loading your number of friends </h2>
447
- </my-friends>
691
+ <ul>
692
+ <li>Ann</li>
693
+ <li>Bob</li>
694
+ </ul>
448
695
  ```
449
- then
696
+
697
+ ### Special Variables in {+each}
698
+
699
+ - `{@}` - The current index (0-based)
450
700
 
451
701
  ```html
452
- <my-friends myId="1234">
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
- ### fragment templates
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
- You can use the [template](#templates) syntax with in a fragment
716
+ Output: `0: a, 1: b, 2: c, `
460
717
 
461
- * The template will use the values pass to it from the render or using a "values" property to match the template string
718
+ ---
462
719
 
463
- **e.g.** assigning values to template from with in the **fragment function**
464
- * `Foo(values){ return{ template:"<p>{txt}</p>", values:{txt:"Ipsum"} }}`
465
- * with `` Html`${{Foo:{}}}` ``
720
+ # Fragments
466
721
 
467
- **or** assigning values to template from with in the **render function**
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
- *Note: the different is whether or not a "values" is returned from the fragment function*
724
+ You define one with a class property starting with a **capital letter**.
472
725
 
473
- **output**
726
+ The fragment function should return an object with:
474
727
 
475
- `<p>Ipsum</p>`
728
+ - **placeholder:** the placeholder to show while resolving
729
+ - **once:** Only generate the fragment once (default: `false`)
476
730
 
477
- **Example:**
731
+ And **one** of the following as the result:
478
732
 
479
- Implementation:
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)**
737
+
738
+ **Example:**
480
739
 
481
740
  ```js
482
- document.registerElement("click-me",class extends hyperElement{
483
- Button(){
484
- return {
485
- template:`<button type="button" class="btn"
486
- onclick={onclick}>{text}</button>`
487
- }// END return
488
- }// END Button
489
- render(Html){
490
- Html`Try ${{Button:{
491
- text:"Click Me",
492
- onclick:()=>alert("Hello!")
493
- }}}`
494
- }// END render
495
- })// END click-me
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
- Output:
763
+ **Live Example of using an [asynchronous fragment](https://codepen.io/codemeasandwich/pen/MdQrVd)**
499
764
 
500
- ```html
501
- <click-me>
502
- Try <button type="button" class="btn">Click Me</button>
503
- </click-me>
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
- #### Asynchronous fragment templates
786
+ **Live Example of [styling](https://codepen.io/codemeasandwich/pen/RmQVKY)**
787
+
788
+ ---
507
789
 
508
- You can also return a [promise] as your `template` property.
790
+ # Connecting to a Data Store
509
791
 
510
- Rewritting the *my-friends* example
792
+ hyper-element integrates with any state management library via `setup()`. The pattern is:
511
793
 
512
- **Example:**
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
513
796
 
514
- Implementation:
797
+ ## Backbone
515
798
 
516
799
  ```js
517
- document.registerElement("my-friends",class extends hyperElement{
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
- FriendCount(user){
814
+ render(Html, { name }) {
815
+ Html`Profile: ${name}`;
816
+ }
817
+ }
818
+ );
819
+ ```
520
820
 
521
- const templatePromise = fetch("/user/"+user.userId+"/friends")
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
- return {
531
- once: true,
532
- placeholder: "loading your number of friends",
533
- template: templatePromise
534
- } // END return
535
- }// END FriendCount
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
- render(Html){
538
- const userId = this.attrs.myId
539
- Html`<h2> ${{FriendCount:userId}} </h2>`
540
- }// END render
541
- }) //END my-friends
835
+ render(Html, { name }) {
836
+ Html`Profile: ${name}`;
837
+ }
838
+ }
839
+ );
542
840
  ```
543
841
 
544
- In this example, the values returned from the promise are used. As the "values" from a fragment function(if provided) takes priority over values passed in from render.
842
+ ## Redux
545
843
 
546
- Output:
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
- ```html
549
- <my-friends myId="1234">
550
- <h2> you have 635 friends </h2>
551
- </my-friends>
852
+ render(Html, { user }) {
853
+ Html`Profile: ${user.name}`;
854
+ }
855
+ }
856
+ );
552
857
  ```
553
858
 
859
+ ---
554
860
 
555
- ## Styling
861
+ # Best Practices
556
862
 
557
- Supports an object as the style attribute. Compatible with React's implementation.
863
+ ## Always Use Html.wire for Lists
558
864
 
559
- **Example:** of centering an element
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
- render(Html){
564
- const style= {
565
- position: "absolute",
566
- top: "50%", left: "50%",
567
- marginRight: "-50%",
568
- transform: "translate(-50%, -50%)"
569
- }//END style
570
- Html`<div style=${style}> center </div>`
571
- }//END render
871
+ // BAD - XSS vulnerability and poor performance
872
+ Html`<ul>${users.map((u) => `<li>${u.name}</li>`)}</ul>`;
873
+ ```
572
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 = '';
882
+
883
+ // GOOD - assignment triggers attribute update
884
+ this.dataset.user = { name: '' };
573
885
  ```
574
886
 
575
- # Example of connecting to a data store
887
+ ## Type Coercion Reference
576
888
 
577
- ## backbone
889
+ | Source | Supported Types |
890
+ | -------------- | ------------------------------ |
891
+ | `this.attrs` | Number |
892
+ | `this.dataset` | Object, Array, Number, Boolean |
893
+
894
+ ## Cleanup Resources in setup()
895
+
896
+ Always return a cleanup function when using resources that need disposal:
578
897
 
579
898
  ```js
580
- var user = new (Backbone.Model.extend({
581
- defaults: {
582
- name: 'Guest User',
583
- }
584
- }));//END Backbone.Model.extend
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
- document.registerElement("my-profile", class extends hyperElement{
918
+ ```bash
919
+ git clone https://github.com/codemeasandwich/hyper-element.git
920
+ cd hyper-element
921
+ ```
588
922
 
589
- setup(setupStore){
590
- user.on("change",setupStore(user.toJSON.bind(user)));
591
- // OR user.on("change",setupStore(()=>user.toJSON()));
592
- }//END setup
923
+ 2. Install dependencies:
593
924
 
594
- render(Html,{name}){
595
- Html`Profile: ${name}`
596
- }//END render
597
- })//END my-profile
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
- ## mobx
972
+ ## Building
601
973
 
602
- ```js
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
- document.registerElement("my-profile", class extends hyperElement{
987
+ The project uses a modular pre-commit hook system located in `.hooks/`. When you commit, the following checks run automatically:
609
988
 
610
- setup(setupStore){
611
- mobx.autorun(setupStore(user));
612
- }// END setup
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
- render(Html,{name}){
615
- Html`Profile: ${name}`
616
- }// END render
617
- })//END my-profile
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
- ## redux
1006
+ ## Code Style
621
1007
 
622
- ```js
623
- document.registerElement("my-profile", class extends hyperElement{
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
- setup(setupStore){
626
- store.subcribe(setupStore(store.getState)
627
- }// END setup
1012
+ Run formatting manually:
628
1013
 
629
- render(Html,{user}){
630
- Html`Profile: ${user.name}`
631
- }// END render
632
- })// END my-profile
1014
+ ```bash
1015
+ npm run format:fix
633
1016
  ```
634
- [shadow-dom]:https://developers.google.com/web/fundamentals/web-components/shadowdom
635
- [innerHTML]:https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
636
- [hyperHTML]:https://viperhtml.js.org/hyper.html
637
- [Custom Elements]:https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements
638
- [Test system]:https://jsfiddle.net/codemeasandwich/k25e6ufv/36/
639
- [promise]:https://scotch.io/tutorials/javascript-promises-for-dummies#understanding-promises
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