Installation
Install via npm:
npm install domx
Or use via CDN:
<script src="https://unpkg.com/domx"></script>
For htmx integration:
<script src="https://unpkg.com/domx/dist/domx-htmx.min.js"></script>
Quick Start
Import the functions you need:
import { collect, apply, observe } from 'domx';
// Define a manifest
const manifest = {
username: { selector: '#username', read: 'value', write: 'value' },
rememberMe: { selector: '#remember', read: 'checked', write: 'checked' }
};
// Collect state from DOM
const state = collect(manifest);
// { username: "alice", rememberMe: true }
// Apply state to DOM
apply(manifest, { username: "bob" });
// Observe changes
const unsubscribe = observe(manifest, (state) => {
console.log('State changed:', state);
});
// Stop observing
unsubscribe();
The Manifest
The manifest is an object that maps state labels to DOM locations. Each entry specifies:
selector- CSS selector to find element(s)read- How to extract value from elementwrite- How to set value on element (optional)watch- Override auto-detected watch event (optional)
Read/Write Shortcuts
| Shortcut | Read | Write |
|---|---|---|
"value" |
el.value |
el.value = x |
"checked" |
el.checked |
el.checked = x |
"text" |
el.textContent |
el.textContent = x |
"attr:name" |
el.getAttribute('name') |
el.setAttribute('name', x) |
"data:name" |
el.dataset.name |
el.dataset.name = x |
| Function | Custom extractor | Custom writer |
Custom Functions
const manifest = {
combined: {
selector: '#thing',
read: (el) => `${el.dataset.foo}-${el.dataset.bar}`,
write: (el, val) => {
const [foo, bar] = val.split('-');
el.dataset.foo = foo;
el.dataset.bar = bar;
}
}
};
collect(manifest)
Reads current DOM state based on manifest. Returns object with labels as keys.
const state = collect(manifest);
// { searchQuery: "hello", sortDir: "asc" }
Multiple elements: When selector matches multiple elements, returns an array:
const manifest = {
tags: { selector: '.tag', read: 'text' }
};
collect(manifest);
// { tags: ["JavaScript", "TypeScript", "Python"] }
apply(manifest, state)
Writes state values to DOM. Only processes entries with write key.
apply(manifest, { username: "alice", theme: "dark" });
observe(manifest, callback)
Watches DOM for changes and calls callback with full state. Auto-detects watch mechanism from read type. Returns unsubscribe function.
const unsubscribe = observe(manifest, (state) => {
console.log('State changed:', state);
});
// Later: stop observing
unsubscribe();
Auto-detection:
"value"→ listens toinputevent"checked"→ listens tochangeevent"attr:*","data:*","text"→ uses MutationObserver
on(callback)
Low-level subscription to raw MutationRecords. For framework integration.
const unsubscribe = on((mutations) => {
// Process raw MutationRecords
});
send(url, manifest, opts?)
Collects state, caches to localStorage, and sends via fetch.
const response = await send('/api/save', manifest, {
headers: { 'X-Custom': 'value' }
});
Security: Cached state in localStorage is accessible to any script on the same domain. Avoid including sensitive data in manifests.
replay()
Re-sends cached request (for page refresh recovery). Returns null if no valid cache or cache expired (5 minutes).
// On page load
const response = await replay();
if (response?.ok) {
const html = await response.text();
container.innerHTML = html;
}
clearCache()
Clears the cached request from localStorage.
clearCache();
htmx Integration - Setup
Include the htmx extension after htmx:
<script src="https://unpkg.com/htmx.org"></script>
<script src="https://unpkg.com/domx/dist/domx-htmx.min.js"></script>
<script>
const manifest = {
searchQuery: { selector: '#search', read: 'value' },
sortDir: { selector: '[data-sort]', read: 'attr:data-sort-dir' }
};
</script>
<body hx-ext="domx" dx-manifest="manifest">
...
</body>
htmx Attributes
| Attribute | Description |
|---|---|
dx-manifest |
Manifest object name or inline JSON |
dx-cache |
Enable localStorage caching ("true"/"false") |
Security: dx-manifest attributes should be server-rendered, not user-settable, to prevent code injection.
htmx Events
| Event | Description |
|---|---|
dx:change |
Fired when any observed state changes. Use with hx-trigger="dx:change" |
dx:replayed |
Fired on document.body after successful replay on page load |
<!-- Auto-search when state changes -->
<div hx-post="/api/search"
hx-trigger="dx:change"
hx-target="#results">
<input id="search" type="text">
<div id="results"></div>
</div>