Skip to content

Autosave

The unsaved-changes guard warns before data is lost; autosave makes the warning mostly moot — a crash costs at most one debounce window of typing. Zero new JavaScript: a request-owning <div> inside the form, three htmx trigger modifiers, and a small server contract for drafts. Covered by the versioning policy.

Also known as: draft saving, auto-save.

Type in the title and pause — after the 2 s debounce the draft status stamps. Save posts the record and the dirty guard turns clean.

Type in the title, then pause two seconds.

<form id="report" data-hc-dirty-guard
data-hx-post="/reports/42"
data-hx-target="#status" data-hx-swap="innerHTML">
<div class="hc-field">
<label class="hc-field__label" for="title">Title</label>
<input class="hc-input" id="title" name="title" value="Quarterly report">
</div>
<div data-hx-post="/reports/42/draft"
data-hx-include="closest form"
data-hx-trigger="input from:closest form changed delay:2s"
data-hx-target="#draft-status" data-hx-swap="innerHTML"></div>
<p class="hc-field__hint" id="draft-status" aria-live="polite"></p>
<p class="hc-field__hint" id="status" aria-live="polite"></p>
<button class="hc-button" data-variant="primary" type="submit">Save</button>
</form>

Three modifiers carry the whole loop: from:closest form listens to every field’s bubbling input, changed skips caret-only keystrokes, delay:2s debounces the burst. data-hx-include="closest form" posts the entire form; the empty <div> owns the request and nothing else.

The draft deliberately does not clean the dirty guard: the guard resets only when the form’s own request succeeds (htmx:afterRequest element identity) — a draft is not the record.

CaseResponse
POST …/draft (each debounced burst)200 + a status line (“Draft saved at 14:03:12” — server-timestamped). Drafts are stored raw, never validated
POST … (record save)the mutating-form contract; on success the server deletes the draft
page render with a newer draftthe form rendered from the record + an hc-alert restore banner: Restore (GET …/draft → the whole form from draft values, data-dirty preset) / Discard (DELETE …/draft)
DELETE …/draft200 + empty fragment
no-JSnative PRG; drafts simply never happen

Server-side hygiene is normative in the contract: drop type="password" values from drafts, keep drafts per-user/per-record last-write-wins, purge on record save.

  • #draft-status is aria-live="polite" — keep the message short and stable (“Draft saved at …”), not chatty.
  • The restore banner is a real hc-alert with buttons before the form in DOM order.