domx-dataos
v0.1.0
Published
DOM state observer for DATAOS — collect, apply, observe, and persist DOM state
Maintainers
Readme
domx
DOM state observer for DATAOS — collect, apply, observe, and persist DOM state.
< 1KB minified + gzipped. Zero dependencies.
What is domx?
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.
Security Considerations
⚠️ Important Security Notes
- 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);Installation
npm install domxOr via CDN:
<script src="https://unpkg.com/domx"></script>Quick Start
1. Define a manifest
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' }
};2. Collect state
const state = domx.collect(manifest);
// → { username: "alice", rememberMe: true, theme: "dark" }3. Apply state
domx.apply(manifest, { username: "bob", theme: "light" });
// DOM is updated4. Observe changes
const unsubscribe = domx.observe(manifest, (state) => {
console.log('State changed:', state);
});
// Later: stop observing
unsubscribe();API Reference
collect(manifest)
Reads current DOM state based on manifest. Returns object with labels as keys.
const state = domx.collect(manifest);apply(manifest, state)
Writes state values to DOM. Only processes entries with write key.
domx.apply(manifest, { username: "alice" });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 = domx.observe(manifest, (state) => {
// Called on any relevant DOM change
});on(callback)
Low-level subscription to raw MutationRecords. For framework integration (e.g., genX modules).
const unsubscribe = domx.on((mutations) => {
// Process raw mutations
});send(url, manifest, opts?)
Collects state, caches to localStorage, and sends via fetch.
⚠️ Security Warning: Cached state in localStorage is accessible to any script on the same domain. Avoid including sensitive data in manifests used with this function.
const response = await domx.send('/api/save', manifest, {
headers: { 'X-Custom': 'value' }
});replay()
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;
}clearCache()
Clears the cached request.
domx.clearCache();Manifest Format
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
For complex cases, pass a function:
⚠️ Security Warning: Custom functions have full access to DOM elements. Avoid using unsafe methods like 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;
}
}
};Multiple Elements
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"] }htmx Integration
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>Features
- 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")
Attributes
| Attribute | Description |
|-----------|-------------|
| dx-manifest | Manifest object name or inline JSON |
| dx-cache | Enable localStorage caching ("true"/"false") |
⚠️ Security Warning: dx-manifest attributes should be server-rendered, not user-settable, to prevent potential code injection through JSON parsing or window property access.
Page Refresh Handling
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;
}
});Comparison with stateless (React)
| 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.
Performance
- Single MutationObserver: Regardless of manifest size
- Batched callbacks: Uses
requestAnimationFrameto batch rapid changes - Passive event listeners: For input/change events
- < 1KB: Minified + gzipped
Related Projects
- 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
License
MIT © Adam Zachary Wasserman
