Autosave

Mode

Skin

Theme

Overview & Philosophy

ln-autosave is applied to forms via <form id="..." data-ln-autosave> to protect form drafts against accidental reloads or tab closures. On initialization, it reads stored draft JSON from localStorage and restores field values, dispatching synthetic input and change events so dependent primitives (like ln-validate and ln-autoresize) update in sync. On field focusout or change, it serializes non-sensitive fields—strictly excluding passwords and excluded inputs—and saves the draft, automatically purging storage when the form is submitted or reset.

In ln-ashlar, this draft buffer primitive handles data persistence entirely on the client without heavy network streaming or concurrency conflicts.

The Local Storage Choice

Unlike background sync components (which use IndexedDB), ln-autosave relies strictly on synchronous localStorage. This ensures saving occurs immediately on field boundaries (focusout / change) with zero race-condition window on page navigation. Drafts are keyed uniquely via a combination of URL path and element identifier.

Concerns Scoped Strictly by Layer

Automatic draft persistence and lifecycle synchronization are achieved by layering separate, orthogonal concerns:

  1. The Storage Layer (Browser localStorage): A synchronous, origin-isolated browser storage engine. It persists values perfectly across tab restarts and system reboots, and automatically cleans up after successful form submissions.
  2. The Form Interface (Native Inputs): Forms declare draft buffering declaratively using the data-ln-autosave attribute. Inputs preserve native value formatting, options, and radio sets, leaving markup clean and standard.
  3. The Coordinator Integrator (DOM Event Flow): Because values are restored on load by dispatching synthetic input / change events, sibling primitives like textareas (ln-autoresize) and validation states (ln-validate) re-evaluate themselves automatically.

How the Persistence Flows

  1. When a user interacts with a form, fields fire focusout or change. The component intercepts these events, serializes the active form values, and writes them instantly to the resolved localStorage key.
  2. On document load, the component initializes, detects any matching localStorage entry, parses the JSON payload, populates form fields, and dispatches synthetic events.
  3. When the form is submitted successfully or reset via standard buttons, the component automatically deletes the draft from local storage, leaving a clean workspace.

For the full guide see the ln-autosave README and the architecture reference.

Anatomy

The minimum invocation is one attribute on the form:

<form id="edit-user" data-ln-autosave>
    <input name="name" type="text">
    <input name="email" type="email">
    <button type="submit">Save</button>
</form>

The storage key is ln-autosave:{pathname}:{identifier} — unique per URL + form. The identifier is the value of data-ln-autosave if non-empty, falling back to form.id. Missing both → console.warn and the constructor exits without instantiating.

The pathname and identifier are captured ONCE at construction. Mutating data-ln-autosave or pushing a new History API URL while the form stays in the DOM does NOT re-key the instance. Destroy and re-mount to switch.

For compose-style textareas where the user never blurs, add data-ln-autosave-debounce-input="500" (value in milliseconds) to opt into a debounced input listener that saves after idle time. The focusout/change listeners remain; debounced input is additive.

Attributes reference

AttributeOn elementDescription
data-ln-autosave<form>Marks the form as autosaved. Empty value uses form.id as the storage identifier; non-empty value uses the attribute value instead. If neither resolves, the constructor logs a warning and exits.
data-ln-autosave-clearany descendant buttonClick delegate marker. Removes the localStorage entry on click. Does NOT reset form values. Typical use: a "Cancel" button that closes a modal AND discards the draft (combine with data-ln-modal-close).
data-ln-autosave-debounce-input<form>Opt-in. Attaches a debounced input listener. Value is milliseconds (e.g. "500"); empty attribute defaults to 1000 ms. Useful for compose-style textareas where the user never blurs.
data-ln-autosave-excludeinput / select / textareaOpt-out. Excludes the individual input element from serialization and storage (used for custom ephemeral fields like OTP or CAPTCHA; passwords and file inputs are excluded automatically).

Live demo — fill, close, reopen

Fill in some fields below, then close (or refresh) this tab. When you reopen, the data is restored. Submitting the form clears the saved draft. Open DevTools → Application → Local Storage to watch the ln-autosave:-prefixed key appear and disappear.

Role
Features

Events

All events bubble. Only before-restore is cancelable. detail.target is always the form element.

EventCancelableDetailWhen
ln-autosave:before-restoreyes{ target, data }Inside restore, after parsing localStorage, BEFORE applying values. Calling preventDefault() skips the apply step entirely; :restored does NOT fire afterwards.
ln-autosave:restoredno{ target, data }After populateForm() has written values AND after synthetic input + change have been dispatched on every restored field. ln-validate / ln-autoresize have already re-reacted by this point.
ln-autosave:savedno{ target, data }After localStorage.setItem succeeds. If the write throws (quota exceeded, localStorage disabled), the try/catch swallows silently and NO event fires — there is no :save-failed event.
ln-autosave:clearedno{ target }After localStorage.removeItem runs. Idempotent — fires even if there was nothing in localStorage to remove.

Event log

Interact with the form above to see events here:


							

Read-only properties — el.lnAutosave

The instance lives on the form element as form.lnAutosave:

const form = document.getElementById('edit-user');

form.lnAutosave.dom;     // back-reference to the form element
form.lnAutosave.key;     // the resolved storage key string
                         // e.g. "ln-autosave:/admin/autosave.html:edit-user"

form.lnAutosave.destroy();   // remove all listeners + pending debounce timer,
                             // delete back-reference.
                             // Does NOT clear localStorage.

// To inspect the draft without applying it:
const raw = localStorage.getItem(form.lnAutosave.key);
const draft = raw ? JSON.parse(raw) : null;

// Manual init scan (Shadow DOM, custom document context).
// The shared MutationObserver covers everything inside document.body.
window.lnAutosave(container);

Migration from imperative API

Previous versions exposed .save(), .restore(), and .clear() on the instance. They are gone. The auto-listeners cover every standard case. For periodic saves on long textareas, use data-ln-autosave-debounce-input. For ESC-discard-on-modal, use data-ln-autosave-clear on a button, or remove the localStorage entry directly: localStorage.removeItem(form.lnAutosave.key).

Cancel restore — server data takes priority

The canonical edit-page pattern. The server has already rendered the form with current record values; a stale draft should not override them.

<form id="edit-user" data-ln-autosave data-has-server-data="true" action="/users/42" method="POST">
    <input name="name" type="text" value="Maria"> <!-- server-rendered -->
    <input name="email" type="email" value="maria@example.com">
    <!-- ... -->
</form>
document.addEventListener('ln-autosave:before-restore', function (e) {
    const form = e.detail.target;
    if (form.dataset.hasServerData === 'true') {
        e.preventDefault();                              // do not apply the draft
        // Remove the localStorage entry directly — the imperative .clear() is gone.
        localStorage.removeItem(form.lnAutosave.key);
    }
});

The data-has-server-data attribute is a project convention, not an ln-autosave feature. The listener can inspect any signal — form.dataset.recordId, a hidden input, a non-empty value field.

Modal forms — close-and-discard

Inside a modal, combine data-ln-modal-close with data-ln-autosave-clear on the same button:

<dialog id="invoice-modal" class="ln-modal" data-ln-modal>
    <form id="invoice" data-ln-autosave action="/invoices" method="POST">
        <header>
            <h3>New Invoice</h3>
            <button type="button" aria-label="Close"
                    data-ln-modal-close data-ln-autosave-clear>
                <svg class="ln-icon" aria-hidden="true"><use href="#ln-icon-x"></use></svg>
            </button>
        </header>
        <main>
            <input name="client" type="text" required>
            <input name="amount" type="number" required>
        </main>
        <footer>
            <button type="button" data-ln-modal-close data-ln-autosave-clear>Cancel</button>
            <button type="submit">Create</button>
        </footer>
    </form>
</dialog>

Note: if the user dismisses via ESC (which ln-modal handles natively), the draft is NOT cleared — ESC does not fire submit, reset, or click any clear button. To wire ESC-discard, listen for the modal's close event and remove the entry directly:

modalEl.addEventListener('ln-modal:closed', function () {
    localStorage.removeItem(formEl.lnAutosave.key);
});

Cross-component reality

ln-autosave does NOT import, listen for, or coordinate with any other component's custom events. The platform's input, change, submit, reset are the only connectors. Compositions that "just work":

ComponentCoordination
ln-formNone at the JS level. Both share serializeForm / populateForm from ln-core/helpers.js. The form's submit event is the natural connector.
ln-validateNone directly. After :restored, the synthetic input / change events bubble up; ln-validate's input listener catches them and validates.
ln-autoresizeNone directly. Restored textareas grow to fit because the synthetic input reaches the autoresize listener.
ln-storeNone. Different layer entirely. ln-store uses IndexedDB for record cache; ln-autosave uses localStorage for form drafts.
ln-modalNone directly. A <form data-ln-autosave> inside a modal works without wiring. Cancel button combines data-ln-modal-close + data-ln-autosave-clear.

Long textarea — debounced input

For compose-style forms where users type continuously without blurring a field, add data-ln-autosave-debounce-input to save after a period of idle input:

<form id="post-editor" data-ln-autosave data-ln-autosave-debounce-input="500">
    <div class="form-element">
        <label for="body">Post body</label>
        <textarea id="body" name="body" required></textarea>
    </div>
    <ul class="form-actions">
        <li><button type="submit">Publish</button></li>
    </ul>
</form>

The attribute value is milliseconds. Empty attribute (data-ln-autosave-debounce-input) defaults to 1000 ms. The focusout/change listeners remain — debounced input is additive, not a replacement. The attribute is read once at construction; mutating it later has no effect.