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 element
  • write - 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 to input event
  • "checked" → listens to change event
  • "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>