Documentation
Reference for talkDOM 0.5.0 · Released September 20, 2026.
Installation
Choose whichever method fits your project.
CDN
Add a single script tag. No build tools, no configuration.
<script src="https://cdn.jsdelivr.net/npm/talkdom@0.5.0/dist/talkdom.min.js"></script>
npm
npm install talkdom@0.5.0
The package contains classic browser scripts that expose window.talkDOM. It does not export a named ES module or Node API. Node 24.15+ is needed for development tooling, not browser usage.
That's it. talkDOM is ready to use.
Core Concepts
talkDOM uses two HTML attributes to wire up interactions: receiver and sender.
Receivers
A receiver attribute marks an element as a message target. The value is the receiver's name.
<div receiver="content"></div>
Senders
A sender attribute makes an element dispatch messages on click. Messages follow Smalltalk keyword syntax:
receiver keyword: arg keyword: arg
The first word is the receiver name, followed by keyword-argument pairs.
Example
Click the button to fetch /page.html and insert its content into the receiver:
<div receiver="content"></div>
<button sender="content get: /page.html apply: inner">Load</button>
Names are literal and whitespace-aware. Extra names before the first keyword are aliases; method arguments are not receiver names. talkDOM.receivers(name) returns current matches, including same-turn DOM changes.
Apply Operations
The apply: keyword determines how fetched content is inserted into the receiver. Four modes are available:
| Mode | Behavior |
|---|---|
| inner | Replaces innerHTML |
| text | Sets textContent |
| append | Appends via insertAdjacentHTML |
| outer | Replaces the receiver element itself via outerHTML |
Restricting operations
Use the accepts attribute to restrict which operations a receiver allows:
<div receiver="output" accepts="text inner"></div>
This receiver will reject append and outer operations.
Use notice text: Saved for a literal text update. accepts uses whitespace-separated operation tokens; missing/empty values allow all supported operations. Invalid/denied applies emit error events. Set talkDOM.config.strictApply = true to reject them and stop pipes; legacy promises resolve by default.
HTTP Methods
talkDOM supports four HTTP keywords. Each takes a URL and returns the response text:
get: → GET request
post: → POST request
put: → PUT request
delete: → DELETE request
Combined fetch + apply
Combine an HTTP method with apply: in a single message to fetch and insert in one step:
<!-- GET and replace inner HTML -->
<button sender="feed get: /api/posts apply: inner">Refresh</button>
<!-- POST and append the response -->
<button sender="feed post: /api/posts apply: append">Add</button>
<!-- DELETE and replace outer HTML (removes element) -->
<button sender="item delete: /api/posts/1 apply: outer">Remove</button>
Message Composition
Messages can be composed using pipes and semicolons for complex workflows.
Pipes — sequential chaining
The | operator chains messages sequentially. The result of each step is passed to the next:
<button sender="content get: /search | content apply: inner">
Search
</button>
Semicolons — parallel independent chains
The ; operator runs multiple independent message chains in parallel:
<button sender="sidebar get: /nav apply: inner; main get: /home apply: inner">
Reset
</button>
Both fetches happen simultaneously. Neither depends on the other.
A successful pipe waits for every matching receiver and passes the last receiver’s result forward. Failures stop that pipe while other started work continues. Semicolons, pipes, and whitespace tokens ending in a colon are reserved; quotes/backslashes do not escape them. Use structured deliver arguments for arbitrary data.
URL History
A sender with push-url updates the URL only after its full message succeeds. Cancellation or failure leaves the URL unchanged; already completed independent chains are not rolled back.
<a href="/about" sender="content get: /partials/about.html apply: inner" push-url="/about">About</a>Back/Forward and reload restore receiver markup snapshots, including the initial view, without repeating POST, DELETE, or append commands. Empty push-url values use the first message argument. URLs must resolve to the page origin.
Restoration replaces receiver nodes. Delegate application listeners, keep receiver layout consistent across reloads, and do not rely on snapshots for unsaved form properties or JavaScript state.
Polling
Receivers can automatically refresh their content at a set interval using the poll: keyword in the receiver attribute:
<div receiver="status get: /api/status apply: inner poll: 5s"></div>
This fetches /api/status every 5 seconds and replaces the element's inner HTML with the response.
Polling stops automatically when the element is removed from the DOM. Elements added dynamically (e.g., via fragment injection) are picked up automatically by a MutationObserver — no manual setup needed.
Poller limit
A maximum of 64 concurrent pollers is enforced by default. Adjust via:
talkDOM.maxPollers = 128;
Identical declarations share a poller and use current group members. Edited attributes and outer replacements are reconciled. Ticks do not overlap while work is pending; errors are caught and emit lifecycle events before later ticks recover.
Persistence
Add the persist attribute to a receiver to save its content to localStorage:
<div receiver="preferences" persist></div>
Content is saved after successful apply operations and restored on startup. Outer swaps save the replacement markup. Storage failures warn without failing the DOM update; corrupt entries are ignored. Useful for user preferences, draft content, or UI state that should survive page refreshes.
Keys use talkDOM:<primary name>, shared across pages of the same origin. Storage is plain text; restored markup is trusted and does not recheck accepts. Do not persist secrets.
Server Triggers
Servers can fire client-side talkDOM messages by including the X-TalkDOM-Trigger response header:
X-TalkDOM-Trigger: notifications get: /api/alerts apply: inner
When talkDOM receives a response with this header, it executes the message as if a sender had been clicked.
CORS
For cross-origin requests, the server must expose the header:
Access-Control-Expose-Headers: X-TalkDOM-Trigger
Request Headers
Core requests include X-TalkDOM-Request and the primary receiver name. Sensitive current-page and CSRF headers go only to same-origin or explicitly trusted destinations:
| Header | Description |
|---|---|
| X-TalkDOM-Request | Always present. Identifies the request as coming from talkDOM. |
| X-TalkDOM-Current-URL | The current page URL at the time of the request. |
| X-TalkDOM-Receiver | The name of the receiver that will handle the response. |
CSRF Protection
Same-origin or explicitly trusted POST, PUT, and DELETE requests automatically include an X-CSRF-Token header, read from a meta tag:
<meta name="csrf-token" content="your-token-here">
Set startup configuration before loading core, or edit talkDOM.config later. trustedOrigins uses exact origins, without wildcards:
window.talkDOMConfig = {
trustedOrigins: ["https://api.example.com"],
includeCurrentURL: false,
allowServerTriggers: false,
strictApply: true
};Current-page headers include query/fragment when enabled. Readable server-trigger responses are trusted commands by default, including cross-origin responses; disable allowServerTriggers when that is unsuitable.
Custom Form Requests
Form body selectors are not built into 0.5.0. Register a custom method for payloads and use normal form validation before dispatch. This same-origin example sends FormData and applies a trusted response:
talkDOM.methods["submit:form:"] = async function(el, url, selector) {
const form = document.querySelector(selector);
if (!form || !form.reportValidity()) throw new Error("Invalid form");
const target = new URL(url, document.baseURI);
if (target.origin !== location.origin) throw new Error("Same-origin URL required");
const token = document.querySelector('meta[name="csrf-token"]')?.content;
const response = await fetch(target, {
method: "POST", body: new FormData(form),
headers: token ? {"X-CSRF-Token": token} : {}
});
if (!response.ok) throw new Error("HTTP " + response.status);
return talkDOM.methods["apply:"](el, await response.text(), "inner");
};
// sender="result submit: /save form: #my-form"Custom JSON Requests
Core methods do not serialize JSON. A custom method can accept a structured value through talkDOM.deliver(), avoiding message-string escaping:
talkDOM.methods["save-json:"] = async function(el, payload) {
const token = document.querySelector('meta[name="csrf-token"]')?.content;
const headers = {"Content-Type": "application/json"};
if (token) headers["X-CSRF-Token"] = token;
const response = await fetch("/api/data", {
method: "POST", headers, body: JSON.stringify(payload)
});
if (!response.ok) throw new Error("HTTP " + response.status);
return talkDOM.methods["apply:"](el, await response.text(), "inner");
};
await talkDOM.deliver(element, "save-json:", [{name: "example"}]);Custom Loading State
Loading indicators are an application extension. This site registers the following example once in its shared script:
talkDOM.methods["get:apply:loading:"] = function(el, url, op, cls) {
el.classList.add(cls);
return talkDOM.methods["get:apply:"](el, url, op)
.finally(function() { el.classList.remove(cls); });
};<div receiver="content"></div>
<button sender="content get: /data apply: inner loading: is-loading">Load</button>Define the is-loading class in your CSS. The built-in method list does not include loading:.
Lifecycle Events
talkDOM dispatches custom events on receiver elements after operations complete:
talkdom:done
Fired after a successful operation. The detail object includes:
{ receiver, selector, args, originalReceiver }
talkdom:error
Fired when an operation fails. The detail object includes:
{ receiver, selector, args, originalReceiver, error }
Example
document.querySelector('[receiver="content"]')
.addEventListener('talkdom:done', (e) => {
console.log('Updated:', e.detail.receiver);
});
Programmatic API
Sending messages from JavaScript
Use talkDOM.send() to dispatch messages programmatically. Returns a Promise:
await talkDOM.send("content get: /api/data apply: inner");
Custom methods
Register custom keywords by adding functions to talkDOM.methods:
talkDOM.methods["highlight:"] = function(el, arg) {
el.style.backgroundColor = arg;
};
// Now usable in sender attributes:
// sender="box highlight: yellow"
Separate sends remain concurrent and can complete out of order. Use structured per-element delivery for data containing message delimiters:
await talkDOM.deliver(element, "text:", ["Literal ; and |"]);
const elements = talkDOM.receivers("content");WebSocket Plugin
The optional WebSocket plugin adds server-push via WebSocket as an alternative to polling. Load it after the core library:
<script src="https://cdn.jsdelivr.net/npm/talkdom@0.5.0/dist/talkdom.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/talkdom@0.5.0/dist/talkdom-ws.min.js"></script>
Receiving
Add ws: as the last keyword on a receiver with a WebSocket URL as its argument:
<div receiver="feed ws: ws://localhost:3000/updates"></div>
The server sends JSON messages to control what gets applied:
{"receiver": "feed", "content": "<p>New post</p>", "op": "append"}
| Field | Required | Description |
|---|---|---|
| receiver | no | Optional name within this connection; omission broadcasts to its subscribers |
| content | no | HTML or text content |
| op | no | inner (default), text, append, outer |
JSON targets only subscribers of the sending connection and preserves zero/false content. Raw command strings remain document-wide; trust the sending endpoint:
feed text: Updated!
Sending
The plugin registers a ws:send: method. The receiver element's value (for inputs) or text content is sent over the WebSocket connection:
<input receiver="chatbox ws: ws://localhost:3000/chat" type="text">
<button sender="chatbox ws:send: ws://localhost:3000/chat">Send</button>
Shared connections
Multiple receivers pointing to the same URL share a single WebSocket connection:
<div receiver="messages ws: ws://localhost:3000/live"></div>
<div receiver="presence ws: ws://localhost:3000/live"></div>
Reconnection
Connections automatically reconnect with exponential backoff (1s initial, 30s max, ±25% jitter). Automatic connections close when their final subscriber disappears. Manually owned connections remain alive and reconnect until explicitly disconnected.
WebSocket events
talkdom:ws:open
Fired when a WebSocket connection opens. Detail: { url }
talkdom:ws:close
Fired when a connection closes. Detail: { url, code, reason }
talkdom:ws:error
Fired on connection error. Detail: { url }
Programmatic WebSocket API
talkDOM.ws.connect("ws://localhost:3000/live");
// Wait until the connection is open before sending.
talkDOM.ws.send("ws://localhost:3000/live", { action: "subscribe" });
talkDOM.ws.disconnect("ws://localhost:3000/live");
talkDOM.ws.connections; // { "ws://...": { state, receivers } }
talkDOM.ws.maxConnections; // default 16
Security
talkDOM does not sanitize HTML. You must ensure that server responses are safe before inserting them into the DOM.
CSRF tokens are automatically read from a <meta name="csrf-token"> tag and sent only to same-origin or explicitly trusted destinations on non-GET requests.
Avoid storing sensitive data in receivers that use the persist attribute, as content is saved to localStorage.
Browser Support
Targets current browsers with native Fetch, Promise, MutationObserver, Map/Set, WeakMap/WeakSet, URL, and DOM Range support. Chromium integration is tested; historical minimum versions are not verified. Internet Explorer is not supported.