# hyper-element

[![npm version](https://img.shields.io/npm/v/hyper-element.svg)](https://www.npmjs.com/package/hyper-element)
[![npm package size](https://img.shields.io/bundlephobia/minzip/hyper-element)](https://bundlephobia.com/package/hyper-element)
[![CI](https://github.com/codemeasandwich/hyper-element/actions/workflows/publish.yml/badge.svg)](https://github.com/codemeasandwich/hyper-element/actions/workflows/publish.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/codemeasandwich/hyper-element)
[![XSS Protected](https://img.shields.io/badge/XSS-Protected-blue.svg)](https://github.com/codemeasandwich/hyper-element)
[![ES6+](https://img.shields.io/badge/ES6+-supported-blue.svg)](https://caniuse.com/es6)

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.

### If you like it, please [★ it on github](https://github.com/codemeasandwich/hyper-element)

# Installation

## npm

```bash
npm install hyper-element
```

### ES6 Modules

```js
import hyperElement from 'hyper-element';

hyperElement('my-elem', (Html, ctx) => Html`Hello ${ctx.attrs.who}!`);
```

### CommonJS

```js
const hyperElement = require('hyper-element');

hyperElement('my-elem', (Html, ctx) => Html`Hello ${ctx.attrs.who}!`);
```

## CDN (Browser)

For browser environments without a bundler:

```html
<script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/build/hyperElement.min.js"></script>
```

The `hyperElement` class will be available globally on `window.hyperElement`.

## Browser Support

hyper-element requires native ES6 class support and the Custom Elements v1 API:

| Browser | Version |
| ------- | ------- |
| Chrome  | 86+     |
| Firefox | 78+     |
| Safari  | 14.1+   |
| Edge    | 86+     |

For older browsers, a [Custom Elements polyfill](https://github.com/webcomponents/polyfills/tree/master/packages/custom-elements) may be required.

## Why hyper-element

- hyper-element is fast & small
  - Zero runtime dependencies - everything is built-in
- With a completely stateless approach, setting and reseting the view is trivial
- Simple yet powerful [Interface](#interface)
- Built in [template](#templates) system to customise the rendered output
- Inline style objects supported (similar to React)
- First class support for [data stores](#connecting-to-a-data-store)
- [Server-side rendering](#server-side-rendering-ssr) with progressive hydration
- Pass `function` to other custom hyper-elements via there tag attribute

# [Live Demo](https://jsfiddle.net/codemeasandwich/k25e6ufv/)

## Live Examples

| Example              | Description                         | Link                                                       |
| -------------------- | ----------------------------------- | ---------------------------------------------------------- |
| Hello World          | Basic element creation              | [CodePen](https://codepen.io/codemeasandwich/pen/VOQpqz)   |
| Attach a Store       | Store integration with setup()      | [CodePen](https://codepen.io/codemeasandwich/pen/VOQWeN)   |
| Templates            | Using the template system           | [CodePen](https://codepen.io/codemeasandwich/pen/LoQLrK)   |
| Child Element Events | Passing functions to child elements | [CodePen](https://codepen.io/codemeasandwich/pen/rgdvPX)   |
| Async Fragments      | Loading content asynchronously      | [CodePen](https://codepen.io/codemeasandwich/pen/MdQrVd)   |
| Styling              | React-style inline styles           | [CodePen](https://codepen.io/codemeasandwich/pen/RmQVKY)   |
| Full Demo            | Complete feature demonstration      | [JSFiddle](https://jsfiddle.net/codemeasandwich/k25e6ufv/) |

---

- [Browser Support](#browser-support)
- [Define a Custom Element](#define-a-custom-element)
- [Functional API](#functional-api)
- [Lifecycle](#lifecycle)
- [Interface](#interface)
  - [render](#render)
  - [Html](#html)
  - [Html.wire](#htmlwire)
  - [Html.lite](#htmllite)
  - [setup](#setup)
  - [this](#this)
- [Advanced Attributes](#advanced-attributes)
- [Templates](#templates)
  - [Basic Syntax](#basic-template-syntax)
  - [Conditionals](#conditionals-if)
  - [Negation](#negation-unless)
  - [Iteration](#iteration-each)
- [Fragments](#fragments)
- [Styling](#styling)
- [Connecting to a Data Store](#connecting-to-a-data-store)
  - [Backbone](#backbone)
  - [MobX](#mobx)
  - [Redux](#redux)
- [Signals](#signals)
  - [signal](#signal)
  - [computed](#computed-1)
  - [effect](#effect)
  - [batch](#batch)
  - [untracked](#untracked)
- [Server-Side Rendering (SSR)](#server-side-rendering-ssr)
  - [Server-Side API](#server-side-api)
  - [Client-Side Hydration](#client-side-hydration)
  - [SSR Configuration](#ssr-configuration)
- [Best Practices](#best-practices)
- [Development](#development)

---

# Define a custom-element

```html
<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdn.jsdelivr.net/npm/hyper-element@latest/build/hyperElement.min.js"></script>
  </head>
  <body>
    <my-elem who="world"></my-elem>
    <script>
      customElements.define(
        'my-elem',
        class extends hyperElement {
          render(Html) {
            Html`hello ${this.attrs.who}`;
          }
        }
      );
    </script>
  </body>
</html>
```

Output

```html
<my-elem who="world"> hello world </my-elem>
```

**Live Example of [helloworld](https://codepen.io/codemeasandwich/pen/VOQpqz)**

---

# Functional API

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.

## Signatures

```js
// 1. Full definition with tag (auto-registers)
hyperElement('my-counter', {
  setup: (ctx, onNext) => {
    /* ... */
  },
  render: (Html, ctx, store) => Html`Count: ${ctx.attrs.count}`,
});

// 2. Shorthand with tag (auto-registers)
hyperElement('hello-world', (Html, ctx) => Html`Hello, ${ctx.attrs.name}!`);

// 3. Definition without tag (returns class for manual registration)
const MyElement = hyperElement({
  render: (Html, ctx) => Html`...`,
});
customElements.define('my-element', MyElement);

// 4. Shorthand without tag (returns class for manual registration)
const Simple = hyperElement((Html, ctx) => Html`Simple!`);
customElements.define('simple-elem', Simple);
```

## Context Object

In the functional API, instead of using `this`, a context object (`ctx`) is passed explicitly to all functions:

| Property             | Description                                    |
| -------------------- | ---------------------------------------------- |
| `ctx.element`        | The DOM element                                |
| `ctx.attrs`          | Parsed attributes with automatic type coercion |
| `ctx.dataset`        | Dataset proxy with automatic type coercion     |
| `ctx.store`          | Store value from setup                         |
| `ctx.wrappedContent` | Text content between the tags                  |

## Example: Counter with Setup

```js
hyperElement('my-counter', {
  setup: (ctx, onNext) => {
    const store = { count: 0 };
    const render = onNext(() => store);

    ctx.increment = () => {
      store.count++;
      render();
    };
  },

  handleClick: (ctx, event) => ctx.increment(),

  render: (Html, ctx, store) => Html`
    <button onclick=${ctx.handleClick}>
      Count: ${store?.count || 0}
    </button>
  `,
});
```

## Example: Timer with Teardown

```js
hyperElement('my-timer', {
  setup: (ctx, onNext) => {
    let seconds = 0;
    const render = onNext(() => ({ seconds }));

    const interval = setInterval(() => {
      seconds++;
      render();
    }, 1000);

    // Return cleanup function
    return () => clearInterval(interval);
  },

  render: (Html, ctx, store) => Html`Elapsed: ${store?.seconds || 0}s`,
});
```

## Backward Compatibility

The functional API is fully backward compatible. Class-based components still work:

```js
class MyElement extends hyperElement {
  render(Html) {
    Html`Hello ${this.attrs.name}!`;
  }
}
customElements.define('my-element', MyElement);
```

---

# Lifecycle

When a hyper-element is connected to the DOM, it goes through the following initialization sequence:

1. Element connected to DOM
2. Unique identifier created
3. MutationObserver attached (watches for attribute/content changes)
4. Fragment methods defined (methods starting with capital letters)
5. Attributes and dataset attached to `this`
6. `setup()` called (if defined)
7. Initial `render()` called

After initialization, the element will automatically re-render when:

- Attributes change
- Content mutations occur (innerHTML/textContent changes)
- Store updates trigger `onStoreChange()`

---

# Interface

## Define your element

There are 2 functions. `render` is _required_ and `setup` is _optional_

## render

This is what will be displayed within your element. Use the `Html` to define your content.

```js
render(Html, store) {
  Html`
    <h1>
      Last updated at ${new Date().toLocaleTimeString()}
    </h1>
  `;
}
```

The second argument `store` contains the value returned from your store function (if using `setup()`).

---

## Html

The primary operation is to describe the complete inner content of the element.

```js
render(Html, store) {
  Html`
    <h1>
      Last updated at ${new Date().toLocaleTimeString()}
    </h1>
  `;
}
```

The `Html` has a primary operation and two utilities: `.wire` & `.lite`

---

## Html.wire

Create reusable sub-elements with object/id binding for efficient rendering.

The wire takes two arguments `Html.wire(obj, id)`:

1. A reference object to match with the created node, allowing reuse of the existing node
2. A string to identify the markup used, allowing the template to be generated only once

### Example: Rendering a List

```js
Html`
  <ul>
    ${users.map((user) => Html.wire(user, ':user_list_item')`<li>${user.name}</li>`)}
  </ul>
`;
```

### Anti-pattern: Inlining Markup as Strings

**BAD example:** ✗

```js
Html`
  <ul>
    ${users.map((user) => `<li>${user.name}</li>`)}
  </ul>
`;
```

This creates a new node for every element on every render, causing:

- **Negative impact on performance**
- **Output will not be sanitized** - potential XSS vulnerability

### Block Syntax

The Html function supports block syntax for iteration and conditionals directly in tagged template literals:

| Syntax                                | Description           |
| ------------------------------------- | --------------------- |
| `{+each ${array}}...{-each}`          | Iterate over arrays   |
| `{+if ${condition}}...{-if}`          | Conditional rendering |
| `{+if ${condition}}...{else}...{-if}` | Conditional with else |
| `{+unless ${condition}}...{-unless}`  | Negated conditional   |

#### {+each} - Iteration

For cleaner list rendering, use the `{+each}...{-each}` syntax:

```js
Html`<ul>{+each ${users}}<li>{name}</li>{-each}</ul>`;
```

This is equivalent to:

```js
Html`<ul>${users.map((user) => Html.wire(user, ':id')`<li>${user.name}</li>`)}</ul>`;
```

The `{+each}` syntax automatically calls `Html.wire()` for each item, ensuring efficient DOM reuse.

**Available variables inside {+each}:**

| Syntax               | Description                               |
| -------------------- | ----------------------------------------- |
| `{name}`             | Access item property                      |
| `{address.city}`     | Nested property access                    |
| `{...}` or `{ ... }` | Current item value (see formatting below) |
| `{@}`                | Current array index (0-based)             |

**Formatting rules for `{...}` output:**

| Type                                | Output                                                |
| ----------------------------------- | ----------------------------------------------------- |
| Primitive (string, number, boolean) | `toString()` and HTML escaped                         |
| Array                               | `.join(",")`                                          |
| Object                              | `JSON.stringify()`                                    |
| Function                            | Called with no args, return value follows these rules |

**Examples:**

```js
// Multiple properties
Html`<ul>{+each ${users}}<li>{name} ({age})</li>{-each}</ul>`;

// Using index
Html`<ol>{+each ${items}}<li>{@}: {title}</li>{-each}</ol>`;

// Nested arrays with {+each {property}}
const categories = [
  { name: 'Fruits', items: [{ title: 'Apple' }, { title: 'Banana' }] },
  { name: 'Veggies', items: [{ title: 'Carrot' }] },
];
Html`
  {+each ${categories}}
    <section>
      <h3>{name}</h3>
      <ul>{+each {items}}<li>{title}</li>{-each}</ul>
    </section>
  {-each}
`;
```

#### {+if} - Conditionals

Render content based on a condition:

```js
Html`{+if ${isLoggedIn}}<p>Welcome back!</p>{-if}`;

// With else
Html`{+if ${isLoggedIn}}<p>Welcome back!</p>{else}<p>Please log in</p>{-if}`;
```

#### {+unless} - Negated Conditionals

Render content when condition is falsy (opposite of {+if}):

```js
Html`{+unless ${hasErrors}}<p>Form is valid</p>{-unless}`;

// With else
Html`{+unless ${isValid}}Invalid input!{else}Looking good!{-unless}`;
```

---

## Html.lite

Create once-off sub-elements for integrating external libraries.

### Example: Wrapping jQuery DatePicker

```js
customElements.define(
  'date-picker',
  class extends hyperElement {
    onSelect(dateText, inst) {
      console.log('selected time ' + dateText);
    }

    Date(lite) {
      const inputElem = lite`<input type="text"/>`;
      $(inputElem).datepicker({ onSelect: this.onSelect });
      return {
        any: inputElem,
        once: true,
      };
    }

    render(Html) {
      Html`Pick a date ${{ Date: Html.lite }}`;
    }
  }
);
```

The `once: true` option ensures the fragment is only generated once, preventing the datepicker from being reinitialized on every render.

---

## Html.raw

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.

**Warning:** Only use with trusted content. Never use with user-provided input as it bypasses XSS protection.

```js
render(Html) {
  const trustedHtml = '<strong>Bold</strong> and <em>italic</em>';
  Html`<div>${Html.raw(trustedHtml)}</div>`;
}
```

Output:

```html
<div><strong>Bold</strong> and <em>italic</em></div>
```

Without `Html.raw()`, the HTML would be escaped:

```html
<div>&lt;strong&gt;Bold&lt;/strong&gt; and &lt;em&gt;italic&lt;/em&gt;</div>
```

---

## setup

The `setup` function wires up an external data-source. This is done with the `attachStore` argument that binds a data source to your renderer.

```js
setup(attachStore) {
  // the getMouseValues function will be called before each render and passed to render
  const onStoreChange = attachStore(getMouseValues);

  // call onStoreChange on every mouse event
  onMouseMove(onStoreChange);

  // cleanup logic
  return () => console.warn('On remove, do component cleanup here');
}
```

**Live Example of [attach a store](https://codepen.io/codemeasandwich/pen/VOQWeN)**

### Re-rendering Without a Data Source

You can trigger re-renders without any external data:

```js
setup(attachStore) {
  setInterval(attachStore(), 1000); // re-render every second
}
```

### Set Initial Values

Pass static data to every render:

```js
setup(attachStore) {
  attachStore({ max_levels: 3 }); // passed to every render
}
```

### Cleanup on Removal

Return a function from `setup` to run cleanup when the element is removed from the DOM:

```js
setup(attachStore) {
  let newSocketValue;
  const onStoreChange = attachStore(() => newSocketValue);
  const ws = new WebSocket('ws://127.0.0.1/data');

  ws.onmessage = ({ data }) => {
    newSocketValue = JSON.parse(data);
    onStoreChange();
  };

  // Return cleanup function
  return ws.close.bind(ws);
}
```

### Multiple Subscriptions

You can trigger re-renders from multiple sources:

```js
setup(attachStore) {
  const onStoreChange = attachStore(user);

  mobx.autorun(onStoreChange); // update when changed (real-time feedback)
  setInterval(onStoreChange, 1000); // update every second (update "the time is now ...")
}
```

---

## this

Available properties and methods on `this`:

| Property              | Description                                                                 |
| --------------------- | --------------------------------------------------------------------------- |
| `this.attrs`          | Attributes on the tag. `<my-elem min="0" max="10" />` = `{ min:0, max:10 }` |
| `this.store`          | Value returned from the store function. _Only updated before each render_   |
| `this.wrappedContent` | Text content between your tags. `<my-elem>Hi!</my-elem>` = `"Hi!"`          |
| `this.element`        | Reference to your created DOM element                                       |
| `this.dataset`        | Read/write access to all `data-*` attributes                                |
| `this.innerShadow`    | Get the innerHTML of the element's rendered content                         |

### this.attrs

Attributes are automatically type-coerced:

| Input     | Output    | Type   |
| --------- | --------- | ------ |
| `"42"`    | `42`      | Number |
| `"3.14"`  | `3.14`    | Number |
| `"hello"` | `"hello"` | String |

### this.dataset

The dataset provides proxied access to `data-*` attributes with automatic JSON parsing:

| Attribute Value          | `this.dataset` Value | Type    |
| ------------------------ | -------------------- | ------- |
| `data-count="42"`        | `42`                 | Number  |
| `data-active="true"`     | `true`               | Boolean |
| `data-active="false"`    | `false`              | Boolean |
| `data-users='["a","b"]'` | `["a", "b"]`         | Array   |
| `data-config='{"x":1}'`  | `{ x: 1 }`           | Object  |

**Example:**

```html
<my-elem data-users='["ann","bob"]'></my-elem>
```

```js
this.dataset.users; // ["ann", "bob"]
```

The `dataset` is a **live reflection**. Changes update the matching data attribute on the element:

```js
this.dataset.user = { name: 'Alice' }; // Updates data-user attribute
```

---

## Advanced Attributes

### Dynamic Attributes with Custom-element Children

Being able to set attributes at run-time should be the same for dealing with a native element and ones defined by hyper-element.

**⚠ To support dynamic attributes on custom elements YOU MUST USE `customElements.define` which requires native ES6 support! Use `/build/hyperElement.min.js`.**

This is what allows for the passing any dynamic attributes from parent to child custom element! You can also pass a `function`, `boolean`, `number`, or `object` to a child element (that extends hyperElement).

**Example:**

```js
window.customElements.define(
  'a-user',
  class extends hyperElement {
    render(Html) {
      const onClick = () => this.attrs.hi('Hello from ' + this.attrs.name);
      Html`${this.attrs.name} <button onclick=${onClick}>Say hi!</button>`;
    }
  }
);

window.customElements.define(
  'users-elem',
  class extends hyperElement {
    onHi(val) {
      console.log('hi was clicked', val);
    }
    render(Html) {
      Html`<a-user hi=${this.onHi} name="Beckett" />`;
    }
  }
);
```

**Live Example of passing an [onclick to a child element](https://codepen.io/codemeasandwich/pen/rgdvPX)**

---

# Templates

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.

To enable templates:

1. Add a `template` attribute to your custom element
2. Define the template markup within your element
3. Call `Html.template(data)` in your render method to populate the template

**Example:**

```html
<my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]'>
  <div>
    <a href="{url}">{name}</a>
  </div>
</my-list>
```

```js
customElements.define(
  'my-list',
  class extends hyperElement {
    render(Html) {
      Html`${this.dataset.json.map((user) => Html.template(user))}`;
    }
  }
);
```

Output:

```html
<my-list template data-json='[{"name":"ann","url":""},{"name":"bob","url":""}]'>
  <div>
    <a href="">ann</a>
  </div>
  <div>
    <a href="">bob</a>
  </div>
</my-list>
```

**Live Example of using [templates](https://codepen.io/codemeasandwich/pen/LoQLrK)**

---

## Basic Template Syntax

| Syntax                             | Description                           |
| ---------------------------------- | ------------------------------------- |
| `{variable}`                       | Simple interpolation                  |
| `{+if condition}...{-if}`          | Conditional rendering                 |
| `{+if condition}...{else}...{-if}` | Conditional with else                 |
| `{+unless condition}...{-unless}`  | Negative conditional (opposite of if) |
| `{+each items}...{-each}`          | Iteration over arrays                 |
| `{@}`                              | Current index in each loop (0-based)  |

---

## Conditionals: {+if}

Show content based on a condition:

```html
<status-elem template>{+if active}Online{else}Offline{-if}</status-elem>
```

```js
customElements.define(
  'status-elem',
  class extends hyperElement {
    render(Html) {
      Html`${Html.template({ active: true })}`;
    }
  }
);
```

Output: `Online`

---

## Negation: {+unless}

Show content when condition is falsy (opposite of +if):

```html
<warning-elem template>{+unless valid}Invalid input!{-unless}</warning-elem>
```

```js
customElements.define(
  'warning-elem',
  class extends hyperElement {
    render(Html) {
      Html`${Html.template({ valid: false })}`;
    }
  }
);
```

Output: `Invalid input!`

---

## Iteration: {+each}

Loop over arrays:

```html
<list-elem template>
  <ul>
    {+each items}
    <li>{name}</li>
    {-each}
  </ul>
</list-elem>
```

```js
customElements.define(
  'list-elem',
  class extends hyperElement {
    render(Html) {
      Html`${Html.template({ items: [{ name: 'Ann' }, { name: 'Bob' }] })}`;
    }
  }
);
```

Output:

```html
<ul>
  <li>Ann</li>
  <li>Bob</li>
</ul>
```

### Special Variables in {+each}

- `{@}` - The current index (0-based)

```html
<nums-elem template>{+each numbers}{@}: {number}, {-each}</nums-elem>
```

```js
customElements.define(
  'nums-elem',
  class extends hyperElement {
    render(Html) {
      Html`${Html.template({ numbers: ['a', 'b', 'c'] })}`;
    }
  }
);
```

Output: `0: a, 1: b, 2: c, `

---

# Fragments

Fragments are pieces of content that can be loaded _asynchronously_.

You define one with a class property starting with a **capital letter**.

The fragment function should return an object with:

- **placeholder:** the placeholder to show while resolving
- **once:** Only generate the fragment once (default: `false`)

And **one** of the following as the result:

- **text:** An escaped string to output
- **any:** Any type of content
- **html:** A html string to output **(not sanitised)**
- **template:** A template string to use **(is sanitised)**

**Example:**

```js
customElements.define(
  'my-friends',
  class extends hyperElement {
    FriendCount(user) {
      return {
        once: true,
        placeholder: 'loading your number of friends',
        text: fetch('/user/' + user.userId + '/friends')
          .then((b) => b.json())
          .then((friends) => `you have ${friends.count} friends`)
          .catch((err) => 'problem loading friends'),
      };
    }

    render(Html) {
      const userId = this.attrs.myId;
      Html`<h2> ${{ FriendCount: userId }} </h2>`;
    }
  }
);
```

**Live Example of using an [asynchronous fragment](https://codepen.io/codemeasandwich/pen/MdQrVd)**

---

# Styling

Supports an object as the style attribute. Compatible with React's implementation.

**Example:** of centering an element

```js
render(Html) {
  const style = {
    position: 'absolute',
    top: '50%',
    left: '50%',
    marginRight: '-50%',
    transform: 'translate(-50%, -50%)',
  };
  Html`<div style=${style}> center </div>`;
}
```

**Live Example of [styling](https://codepen.io/codemeasandwich/pen/RmQVKY)**

---

# Connecting to a Data Store

hyper-element integrates with any state management library via `setup()`. The pattern is:

1. Call `attachStore()` with a function that returns your state
2. Subscribe to your store and call the returned function when state changes

## Backbone

```js
var user = new (Backbone.Model.extend({
  defaults: {
    name: 'Guest User',
  },
}))();

customElements.define(
  'my-profile',
  class extends hyperElement {
    setup(attachStore) {
      user.on('change', attachStore(user.toJSON.bind(user)));
      // OR user.on("change", attachStore(() => user.toJSON()));
    }

    render(Html, { name }) {
      Html`Profile: ${name}`;
    }
  }
);
```

## MobX

```js
const user = observable({
  name: 'Guest User',
});

customElements.define(
  'my-profile',
  class extends hyperElement {
    setup(attachStore) {
      mobx.autorun(attachStore(user));
    }

    render(Html, { name }) {
      Html`Profile: ${name}`;
    }
  }
);
```

## Redux

```js
customElements.define(
  'my-profile',
  class extends hyperElement {
    setup(attachStore) {
      store.subscribe(attachStore(store.getState));
    }

    render(Html, { user }) {
      Html`Profile: ${user.name}`;
    }
  }
);
```

---

# Signals

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.

```js
import { signal, computed, effect, batch, untracked } from 'hyper-element';
```

## signal

Creates a reactive signal that holds a value and notifies subscribers when it changes.

```js
const count = signal(0);

// Read value (tracks dependencies in effects/computed)
console.log(count.value); // 0

// Write value (notifies subscribers)
count.value = 1;

// Read without tracking
count.peek(); // 1

// Subscribe to changes
const unsubscribe = count.subscribe(() => {
  console.log('Count changed:', count.peek());
});
```

## computed

Creates a derived signal that automatically recomputes when its dependencies change. Computation is lazy and cached.

```js
const count = signal(0);
const doubled = computed(() => count.value * 2);

console.log(doubled.value); // 0

count.value = 5;
console.log(doubled.value); // 10

// Read without tracking
doubled.peek(); // 10
```

## effect

Creates a side effect that runs immediately and re-runs whenever its dependencies change. Can return a cleanup function.

```js
const count = signal(0);

// Effect runs immediately, then on every change
const cleanup = effect(() => {
  console.log('Count is:', count.value);

  // Optional cleanup function
  return () => {
    console.log('Cleaning up previous effect');
  };
});

count.value = 1;
// Logs: "Cleaning up previous effect"
// Logs: "Count is: 1"

// Stop the effect
cleanup();
```

## batch

Batches multiple signal updates so effects only run once after all updates complete.

```js
const firstName = signal('John');
const lastName = signal('Doe');

effect(() => {
  console.log(`${firstName.value} ${lastName.value}`);
});
// Logs: "John Doe"

// Without batch: effect would run twice
// With batch: effect runs once after both updates
batch(() => {
  firstName.value = 'Jane';
  lastName.value = 'Smith';
});
// Logs: "Jane Smith" (only once)
```

## untracked

Reads signals without creating dependencies. Useful for reading values in effects without subscribing to changes.

```js
const count = signal(0);
const other = signal('hello');

effect(() => {
  // This dependency IS tracked
  console.log('Count:', count.value);

  // This read is NOT tracked - effect won't re-run when 'other' changes
  const otherValue = untracked(() => other.value);
  console.log('Other:', otherValue);
});

count.value = 1; // Effect re-runs
other.value = 'world'; // Effect does NOT re-run
```

## Using Signals with hyper-element

Signals integrate naturally with hyper-element's setup/render lifecycle:

```js
import hyperElement, { signal, effect } from 'hyper-element';

hyperElement('counter-app', {
  setup: (ctx, onNext) => {
    const count = signal(0);

    // Trigger re-render when count changes
    const stopEffect = effect(() => {
      onNext(() => ({ count: count.value }))();
    });

    // Expose increment method
    ctx.increment = () => count.value++;

    // Cleanup effect on disconnect
    return stopEffect;
  },

  handleClick: (ctx) => ctx.increment(),

  render: (Html, ctx, store) => Html`
    <button onclick=${ctx.handleClick}>
      Count: ${store?.count ?? 0}
    </button>
  `,
});
```

---

# Server-Side Rendering (SSR)

hyper-element supports server-side rendering for faster initial page loads and SEO. The SSR system has two parts:

1. **Server-side API** - Render components to HTML strings in Node.js/Deno/Bun
2. **Client-side hydration** - Capture user interactions during page load and replay them after components register

## Server-Side API

Import SSR functions from the dedicated server entry point:

```js
// Node.js / Bun / Deno
import {
  renderElement,
  renderElements,
  createRenderer,
  ssrHtml,
  escapeHtml,
  safeHtml,
} from 'hyper-element/ssr/server';
```

### renderElement

Render a single component to an HTML string:

```js
const html = await renderElement('user-card', {
  attrs: { name: 'Alice', role: 'Admin' },
  store: { lastLogin: '2024-01-15' },
  render: (Html, ctx) => Html`
    <div class="card">
      <h2>${ctx.attrs.name}</h2>
      <span>${ctx.attrs.role}</span>
      <small>Last login: ${ctx.store.lastLogin}</small>
    </div>
  `,
});

// Result: <user-card name="Alice" role="Admin"><div class="card">...</div></user-card>
```

**Options:**

| Option      | Type       | Description                                           |
| ----------- | ---------- | ----------------------------------------------------- |
| `attrs`     | `object`   | Attributes to pass to the component                   |
| `store`     | `object`   | Store data available in render                        |
| `render`    | `function` | Required render function `(Html, ctx) => Html\`...\`` |
| `shadowDOM` | `boolean`  | Wrap output in Declarative Shadow DOM template        |
| `fragments` | `object`   | Fragment functions for async content                  |

### createRenderer

Create a reusable renderer for a component:

```js
const renderUserCard = createRenderer(
  'user-card',
  (Html, ctx) => Html`
    <div class="card">
      <h2>${ctx.attrs.name}</h2>
    </div>
  `,
  { shadowDOM: false } // default options
);

// Use it multiple times
const html1 = await renderUserCard({ name: 'Alice' });
const html2 = await renderUserCard({ name: 'Bob' });
```

### renderElements

Render multiple components in parallel:

```js
const results = await renderElements([
  { tagName: 'user-card', attrs: { name: 'Alice' }, render: renderFn },
  { tagName: 'user-card', attrs: { name: 'Bob' }, render: renderFn },
]);
// Returns array of HTML strings
```

### ssrHtml

Tagged template literal for rendering HTML strings directly. SVG content is auto-detected when using `<svg>` tags:

```js
const header = ssrHtml`<header><h1>${title}</h1></header>`;
const icon = ssrHtml`<svg viewBox="0 0 24 24"><path d="${pathData}"/></svg>`;
```

### escapeHtml / safeHtml

Utility functions for HTML escaping:

```js
// Escape user input
const safe = escapeHtml('<script>alert("xss")</script>');
// Result: &lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;

// Mark trusted HTML as safe (bypasses escaping)
const trusted = safeHtml('<strong>Bold</strong>');
```

### Fragments in SSR

Fragments work on the server too for async content:

```js
const html = await renderElement('user-profile', {
  attrs: { userId: '123' },
  fragments: {
    FriendCount: async (userId) => {
      const count = await fetchFriendCount(userId);
      return { text: `${count} friends` };
    },
  },
  render: (Html, ctx) => Html`
    <div>
      <h1>Profile</h1>
      <p>${{ FriendCount: ctx.attrs.userId }}</p>
    </div>
  `,
});
```

---

## Client-Side Hydration

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.

### How It Works

```
1. CAPTURE - hyper-element loads in <head>, starts listening for events
2. BUFFER  - User interacts with SSR markup, events are stored
3. REPLAY  - After customElements.define() + first render, events replay
```

### configureSSR

Configure which events to capture (call before components register):

```js
import { configureSSR } from 'hyper-element';

configureSSR({
  events: ['click', 'input', 'change', 'submit'], // Events to capture
  devMode: true, // Show visual indicator during capture (dev only)
});
```

**Default captured events:** `click`, `dblclick`, `input`, `change`, `submit`, `keydown`, `keyup`, `keypress`, `focus`, `blur`, `focusin`, `focusout`, `touchstart`, `touchend`, `touchmove`, `touchcancel`

### Lifecycle Hooks

Components can hook into the hydration process:

```js
customElements.define(
  'my-component',
  class extends hyperElement {
    // Called before events are replayed
    // Return filtered/modified events array
    onBeforeHydrate(bufferedEvents) {
      console.log('Events captured:', bufferedEvents.length);
      // Filter out old events
      return bufferedEvents.filter((e) => Date.now() - e.timestamp < 5000);
    }

    // Called after all events have been replayed
    onAfterHydrate() {
      console.log('Hydration complete!');
    }

    render(Html) {
      Html`<button>Click me</button>`;
    }
  }
);
```

### BufferedEvent Structure

Each captured event contains:

```ts
interface BufferedEvent {
  type: string; // 'click', 'input', etc.
  timestamp: number; // When event occurred
  targetPath: string; // DOM path like 'DIV:0/BUTTON:1'
  detail: object; // Event-specific properties
}
```

### State Preservation

The hydration system automatically preserves:

- **Form values** - Input, textarea, select values via `input` events
- **Checkbox/radio state** - Checked state captured and restored
- **Scroll position** - Scroll positions within components

---

## SSR Configuration

### Full Configuration Reference

```js
import { configureSSR } from 'hyper-element';

configureSSR({
  // Events to capture during SSR hydration
  events: [
    'click',
    'dblclick',
    'input',
    'change',
    'submit',
    'keydown',
    'keyup',
    'keypress',
    'focus',
    'blur',
    'focusin',
    'focusout',
    'touchstart',
    'touchend',
    'touchmove',
    'touchcancel',
  ],

  // Show orange "SSR Capture Active" badge (development only)
  devMode: false,
});
```

---

## Complete SSR Example

**Server (Node.js):**

```js
import { renderElement } from 'hyper-element/ssr/server';

const html = await renderElement('todo-list', {
  attrs: { title: 'My Tasks' },
  store: {
    items: [
      { id: 1, text: 'Learn SSR', done: false },
      { id: 2, text: 'Build app', done: false },
    ],
  },
  render: (Html, ctx) => Html`
    <h1>${ctx.attrs.title}</h1>
    <ul>
      {+each ${ctx.store.items}}
        <li data-id="{id}">{text}</li>
      {-each}
    </ul>
  `,
});

// Serve full HTML page
res.send(`
<!DOCTYPE html>
<html>
<head>
  <script src="/hyper-element.min.js"></script>
</head>
<body>
  ${html}
  <script src="/app.js"></script>
</body>
</html>
`);
```

**Client (app.js):**

```js
import hyperElement, { configureSSR } from 'hyper-element';

// Optional: configure before components register
configureSSR({ devMode: true });

// Register the component - hydration happens automatically
hyperElement('todo-list', {
  onBeforeHydrate(events) {
    console.log('Replaying', events.length, 'events');
    return events;
  },

  onAfterHydrate() {
    console.log('Todo list hydrated!');
  },

  render: (Html, ctx, store) => Html`
    <h1>${ctx.attrs.title}</h1>
    <ul>
      {+each ${store.items}}
        <li data-id="{id}">{text}</li>
      {-each}
    </ul>
  `,
});
```

---

# Best Practices

## Always Use Html.wire for Lists

When rendering lists, always use `Html.wire()` to ensure proper DOM reuse and prevent XSS vulnerabilities:

```js
// GOOD - Safe and efficient
Html`<ul>${users.map((u) => Html.wire(u, ':item')`<li>${u.name}</li>`)}</ul>`;

// BAD - XSS vulnerability and poor performance
Html`<ul>${users.map((u) => `<li>${u.name}</li>`)}</ul>`;
```

## Dataset Updates Require Assignment

The `dataset` works by reference. To update an attribute you must use **assignment**:

```js
// BAD - mutation doesn't trigger attribute update
this.dataset.user.name = '';

// GOOD - assignment triggers attribute update
this.dataset.user = { name: '' };
```

## Type Coercion Reference

| Source         | Supported Types                |
| -------------- | ------------------------------ |
| `this.attrs`   | Number                         |
| `this.dataset` | Object, Array, Number, Boolean |

## Cleanup Resources in setup()

Always return a cleanup function when using resources that need disposal:

```js
setup(attachStore) {
  const interval = setInterval(attachStore(), 1000);
  return () => clearInterval(interval); // Cleanup on removal
}
```

---

# Development

## Prerequisites

- **Node.js** 20 or higher
- **npm** (comes with Node.js)

## Setup

1. Clone the repository:

   ```bash
   git clone https://github.com/codemeasandwich/hyper-element.git
   cd hyper-element
   ```

2. Install dependencies:

   ```bash
   npm install
   ```

   This also installs the pre-commit hooks automatically via the `prepare` script.

## Available Scripts

| Command               | Description                                       |
| --------------------- | ------------------------------------------------- |
| `npm run build`       | Build minified production bundle with source maps |
| `npm test`            | Run Playwright tests with coverage                |
| `npm run test:ui`     | Run tests with Playwright UI for debugging        |
| `npm run test:headed` | Run tests in headed browser mode                  |
| `npm run kitchensink` | Start local dev server for examples               |
| `npm run lint`        | Run ESLint to check for code issues               |
| `npm run format`      | Check Prettier formatting                         |
| `npm run format:fix`  | Auto-fix Prettier formatting issues               |
| `npm run release`     | Run the release script (maintainers only)         |

## Project Structure

```
hyper-element/
├── src/                     # Source files (ES modules)
│   ├── attributes/          # Attribute handling
│   ├── core/                # Core utilities
│   ├── html/                # HTML tag functions
│   ├── lifecycle/           # Lifecycle hooks
│   ├── render/              # Custom render core (uhtml-inspired)
│   ├── signals/             # Reactive primitives (signal, computed, effect)
│   ├── template/            # Template processing
│   ├── utils/               # Shared utilities
│   └── index.js             # Main export
├── build/
│   ├── hyperElement.min.js  # Minified production build
│   └── hyperElement.min.js.map
├── kitchensink/             # Test suite
│   ├── kitchensink.spec.js  # Playwright test runner
│   └── *.html               # Test case files
├── example/                 # Example project
├── docs/                    # Documentation
├── .hooks/                  # Git hooks
│   ├── pre-commit           # Main hook orchestrator
│   ├── commit-msg           # Commit message validator
│   └── pre-commit.d/        # Modular validation scripts
└── scripts/
    └── publish.sh           # Release script
```

## Building

The build process uses [esbuild](https://esbuild.github.io/) for fast, minimal output:

```bash
npm run build
```

This produces:

- `build/hyperElement.min.js` - Minified bundle (~6.2 KB)
- `build/hyperElement.min.js.map` - Source map for debugging

## Pre-commit Hooks

The project uses a modular pre-commit hook system located in `.hooks/`. When you commit, the following checks run automatically:

1. **ESLint** - Code quality checks
2. **Prettier** - Code formatting
3. **Build** - Ensures the build succeeds
4. **Coverage** - Enforces 100% test coverage
5. **JSDoc** - Documentation validation
6. **Docs** - Documentation completeness

If any check fails, the commit is blocked until the issue is fixed.

### Installing Hooks Manually

If hooks weren't installed automatically:

```bash
npm run hooks:install
```

## Code Style

- **Prettier** for formatting (2-space indent, single quotes, trailing commas)
- **ESLint** for code quality
- All files are automatically checked on commit

Run formatting manually:

```bash
npm run format:fix
```

## Testing

hyper-element uses a **two-phase test workflow** to ensure both source quality and build integrity:

### Phase 1: Source Coverage

```bash
npm run test:src
```

- Loads `src/` directly via ES modules + import maps
- Collects V8 coverage on source files
- Generates HTML report at `coverage/index.html`
- Runs SSR tests with coverage
- **Requires 100% coverage** on all metrics

### Phase 2: Bundle Verification

```bash
npm run test:bundle
```

- Loads built `build/hyperElement.min.js`
- Verifies nothing broke during bundling
- No coverage collected (just verification)

### Full Test Suite

```bash
npm test
```

Runs both phases sequentially: source coverage first, then bundle verification.

### Viewing Coverage Report

After running tests, open the HTML coverage report:

```bash
open coverage/index.html
```

This shows:

- File-by-file coverage breakdown
- Line-by-line highlighting of covered/uncovered code
- Statement, branch, and function metrics

### Test Files

Tests are located in `kitchensink/` and run via Playwright. See `kitchensink/kitchensink.spec.js` for the test suite.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines.

---

[shadow-dom]: https://developers.google.com/web/fundamentals/web-components/shadowdom
[innerHTML]: https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML
[Custom Elements]: https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements
[Test system]: https://jsfiddle.net/codemeasandwich/k25e6ufv/36/
[promise]: https://scotch.io/tutorials/javascript-promises-for-dummies#understanding-promises
