@tempots/dom
Version:
Fully-typed frontend framework alternative to React and Angular
240 lines (169 loc) • 7.02 kB
Markdown
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.
[](https://www.npmjs.com/package/@tempots/dom)
[](https://github.com/fponticelli/tempots/blob/main/LICENSE)
[](https://codecov.io/gh/fponticelli/tempots)
[](https://github.com/fponticelli/tempots/actions)
```bash
npm install @tempots/dom
yarn add @tempots/dom
pnpm add @tempots/dom
```
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 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!
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')
)
```
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'
)
```
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')
)
)
```
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)))
```
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
For comprehensive documentation, visit the [Tempo Documentation Site](https://tempo-ts.com/).
Check out the [examples directory](https://github.com/fponticelli/tempots/tree/main/demo) for complete examples.
This package is licensed under the Apache License 2.0.