# Tempo DOM (@tempots/dom)

Tempo DOM is a lightweight UI framework for building web applications with TypeScript. It provides a simple, functional approach to creating reactive user interfaces with direct DOM manipulation. This package has no dependencies and serves as the core of the Tempo ecosystem.

[![npm version](https://img.shields.io/npm/v/@tempots/dom.svg)](https://www.npmjs.com/package/@tempots/dom)
[![license](https://img.shields.io/npm/l/@tempots/dom.svg)](https://github.com/fponticelli/tempots/blob/main/LICENSE)
[![codecov](https://codecov.io/gh/fponticelli/tempots/branch/main/graph/badge.svg)](https://codecov.io/gh/fponticelli/tempots)
[![CI](https://github.com/fponticelli/tempots/workflows/CI/badge.svg)](https://github.com/fponticelli/tempots/actions)

## Installation

```bash
# npm
npm install @tempots/dom

# yarn
yarn add @tempots/dom

# pnpm
pnpm add @tempots/dom
```

## Key Concepts

### Renderables

Renderables are the building blocks of Tempo applications. A Renderable is a function that:

1. Takes a context (typically a DOM context)
2. Performs some operations on that context (like creating DOM elements)
3. Returns a cleanup function

```typescript
import { html, render } from '@tempots/dom'

// Create a simple renderable
const HelloWorld = html.h1('Hello World')

// Render it to the DOM
render(HelloWorld, document.body)
```

### Signals

Signals are reactive values that automatically update the UI when they change:

```typescript
import { html, render, prop, on } from '@tempots/dom'

function Counter() {
  // Create a reactive state
  const count = prop(0)

  return html.div(
    html.div('Count: ', count.map(String)),  // ✨ Auto-disposed
    html.button(
      on.click(() => count.value--),
      'Decrement'
    ),
    html.button(
      on.click(() => count.value++),
      'Increment'
    )
  )
}

render(Counter(), document.body)
```

**Automatic Memory Management:** Signals created within renderables are automatically tracked and disposed when the component is removed from the DOM. This includes:
- Signals created with `prop()`, `signal()`, `computed()`
- Derived signals from `.map()`, `.filter()`, `.flatMap()`, etc.
- No manual `OnDispose()` calls needed!

### HTML Elements

Tempo provides a convenient way to create HTML elements using the `html` object:

```typescript
import { html } from '@tempots/dom'

const myDiv = html.div(
  html.h1('Title'),
  html.p('Paragraph text'),
  html.button('Click me')
)
```

### Attributes and Events

Add attributes and event handlers to elements:

```typescript
import { html, attr, on } from '@tempots/dom'

const button = html.button(
  attr.class('primary-button'),
  attr.disabled(false),
  on.click(() => console.log('Button clicked')),
  'Click Me'
)
```

### Delegated Events

For containers with many similar children (e.g., lists rendered with `ForEach`), use `delegate` to attach a single event listener on the container instead of one per child:

```typescript
import { html, delegate, ForEach, prop } from '@tempots/dom'

const items = prop(['Apple', 'Banana', 'Cherry'])

html.ul(
  delegate.click('li', (event) => {
    const li = (event.target as Element).closest('li')!
    console.log('Clicked:', li.textContent)
  }),
  ForEach(items, (item) => html.li(item))
)
```

`delegate` uses the same proxy pattern as `on` — all standard events are available. It matches children using `Element.closest()` with a CSS selector. Non-bubbling events (`focus`, `blur`, `mouseenter`, `mouseleave`) should use `on` instead.

### Conditional Rendering

Render content conditionally:

```typescript
import { html, When, prop } from '@tempots/dom'

const isLoggedIn = prop(false)

const greeting = html.div(
  When(
    isLoggedIn,
    () => html.span('Welcome back!'),
    () => html.span('Please log in')
  )
)
```

### Lists and Iterations

Render lists of items:

```typescript
import { html, ForEach, prop } from '@tempots/dom'

const items = prop(['Apple', 'Banana', 'Cherry'])

const list = html.ul(ForEach(items, item => html.li(item)))
```

### Keyed Lists

When list items have stable identities (e.g., database IDs), use `KeyedForEach` for efficient reconciliation. Unlike `ForEach` which tracks items by index, `KeyedForEach` tracks items by a user-provided key function — reusing both DOM nodes and signal identities across reorders:

```typescript
import { html, KeyedForEach, prop } from '@tempots/dom'

const todos = prop([
  { id: 1, text: 'Buy groceries' },
  { id: 2, text: 'Walk the dog' },
  { id: 3, text: 'Read a book' },
])

const list = html.ul(
  KeyedForEach(
    todos,
    (todo) => todo.id,                    // key function
    (todo, pos) => html.li(               // item renderer
      todo.map((t) => t.text)
    ),
    () => html.hr()                       // optional separator
  )
)
```

When `todos` is reordered, `KeyedForEach` moves existing DOM elements instead of recreating them. Each item receives a `KeyedPosition` with fully reactive position fields (`index`, `counter`, `isFirst`, `isLast`, `isEven`, `isOdd`) that update automatically when items move.

### Storage-Backed Props

Tempo provides helpers that persist reactive state to Web Storage through `storedProp`,
`localStorageProp`, and `sessionStorageProp`.

```typescript
const theme = localStorageProp({
  key: 'tempo:theme',
  defaultValue: 'light',
  syncTabs: true, // the default
})

theme.value = 'dark' // automatically persisted and broadcast to other tabs
```

When `syncTabs` is enabled (the default), Tempo uses the Broadcast Channel API to
propagate updates across browser contexts that share the same origin. If the API is not
available, or if you prefer to isolate storage changes per tab, set `syncTabs: false`.

All values pass through the provided `serialize`/`deserialize` functions before being
stored, so cross-tab updates respect custom serialization logic as well.

#### Reactive Storage Keys

Storage keys can be reactive, allowing you to dynamically change which storage location
a prop reads from and writes to:

```typescript
const userId = prop('user123')

// Storage key changes when userId changes
const userTheme = localStorageProp({
  key: userId.map(id => `user:${id}:theme`),
  defaultValue: 'light',
  onKeyChange: 'load', // default: load value from new key
})

userTheme.value = 'dark' // stored at 'user:user123:theme'

userId.value = 'user456' // switches to 'user:user456:theme' and loads its value
```

The `onKeyChange` option controls what happens when the key changes:

- `'load'` (default): Load value from the new storage key
- `'migrate'`: Move the current value to the new key
- `'keep'`: Keep the current value without loading from the new key

## Documentation

For comprehensive documentation, visit the [Tempo Documentation Site](https://tempo-ts.com/).

## Examples

Check out the [examples directory](https://github.com/fponticelli/tempots/tree/main/demo) for complete examples.

## License

This package is licensed under the Apache License 2.0.
