DOM state observer for DATAOS — collect, apply, observe, and persist DOM state.
< 1KB minified + gzipped. Zero dependencies.
domx implements the DATAOS principle: DOM as the single source of truth.
Instead of syncing JavaScript state with DOM state (and inevitably getting them out of sync), domx reads state directly from the DOM when needed. No Redux. No MobX. No useState. Just the DOM.
- Avoid storing sensitive data: State cached to localStorage is accessible to any script on the same domain. Do not include passwords, tokens, or other sensitive information in manifests used with
send()or HTMX caching. - Use static manifests: Define manifests in code, not dynamically from user input, to prevent selector injection attacks.
- Safe custom functions: When using custom
read/writefunctions, avoid unsafe DOM methods likeinnerHTML. Stick to the provided shortcuts for security. - Server-controlled attributes: Ensure
dx-manifestattributes are rendered server-side, not set by user input, to prevent code injection.
// Define what state lives where in the DOM
const manifest = {
searchQuery: { selector: '#search', read: 'value' },
sortDir: { selector: '[data-sort]', read: 'attr:data-sort-dir' },
filters: { selector: '.filter.active', read: 'data:filter' }
};
// Collect state from DOM
const state = domx.collect(manifest);
// → { searchQuery: "hello", sortDir: "asc", filters: ["status", "priority"] }
// Send to server
const response = await domx.send('/api/search', manifest);npm install domxOr via CDN:
<script src="https://unpkg.com/domx"></script>The manifest maps state labels to DOM selectors and read/write methods:
const manifest = {
username: { selector: '#username', read: 'value', write: 'value' },
rememberMe: { selector: '#remember', read: 'checked', write: 'checked' },
theme: { selector: '[data-theme]', read: 'data:theme', write: 'data:theme' }
};const state = domx.collect(manifest);
// → { username: "alice", rememberMe: true, theme: "dark" }domx.apply(manifest, { username: "bob", theme: "light" });
// DOM is updatedconst unsubscribe = domx.observe(manifest, (state) => {
console.log('State changed:', state);
});
// Later: stop observing
unsubscribe();Reads current DOM state based on manifest. Returns object with labels as keys.
const state = domx.collect(manifest);Writes state values to DOM. Only processes entries with write key.
domx.apply(manifest, { username: "alice" });Watches DOM for changes and calls callback with full state. Auto-detects watch mechanism from read type. Returns unsubscribe function.
const unsubscribe = domx.observe(manifest, (state) => {
// Called on any relevant DOM change
});Low-level subscription to raw MutationRecords. For framework integration (e.g., genX modules).
const unsubscribe = domx.on((mutations) => {
// Process raw mutations
});Collects state, caches to localStorage, and sends via fetch.
const response = await domx.send('/api/save', manifest, {
headers: { 'X-Custom': 'value' }
});Re-sends cached request (for page refresh recovery). Returns null if no valid cache.
// On page load
const response = await domx.replay();
if (response?.ok) {
const html = await response.text();
container.innerHTML = html;
}Clears the cached request.
domx.clearCache();| 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 |
For complex cases, pass a function:
innerHTML to prevent XSS attacks.
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;
}
}
};When selector matches multiple elements, collect() returns an array:
const manifest = {
tags: { selector: '.tag', read: 'text' }
};
const state = domx.collect(manifest);
// → { tags: ["JavaScript", "TypeScript", "Python"] }domx includes an htmx extension for seamless integration:
<script src="domx.js"></script>
<script src="domx-htmx.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" dx-cache="true">
<input id="search" type="text">
<button data-sort data-sort-dir="asc" hx-post="/api/search" hx-trigger="click">
Search
</button>
</body>- Auto state collection: State is automatically added to request parameters
- dx-cache: When true, caches state to localStorage and auto-replays on page refresh (
⚠️ avoid sensitive data) - dx:change event: Fires when any observed state changes (use with
hx-trigger="dx:change")
| Attribute | Description |
|---|---|
dx-manifest |
Manifest object name or inline JSON |
dx-cache |
Enable localStorage caching ("true"/"false") |
dx-manifest attributes should be server-rendered, not user-settable, to prevent potential code injection through JSON parsing or window property access.
domx solves the "lost state on refresh" problem:
- Before request:
send()caches state to localStorage - On refresh:
replay()re-sends the cached request - Server responds: Fresh HTML with correct state
// On page load
document.addEventListener('DOMContentLoaded', async () => {
const response = await domx.replay();
if (response?.ok) {
const html = await response.text();
document.getElementById('container').innerHTML = html;
}
});| stateless (React) | domx (Vanilla) |
|---|---|
useDomState(manifest) |
collect(manifest) |
useDomValue() setter |
apply(manifest, state) |
| Hook re-render on mutation | observe(manifest, callback) |
Both implement DATAOS principles. Use stateless for React apps, domx for vanilla JS or htmx apps.
- Single MutationObserver: Regardless of manifest size
- Batched callbacks: Uses
requestAnimationFrameto batch rapid changes - Passive event listeners: For input/change events
- < 1KB: Minified + gzipped
- DATAOS - The philosophy behind domx
- stateless - React implementation of DATAOS
- genX - Declarative HTML formatting library (uses domx)
- htmx - High power tools for HTML
- multicardz - DATAOS in production
MIT © Adam Zachary Wasserman