Form Auto-Save Progress Development
Long forms — surveys, applications, questionnaires — lose users if they accidentally close tab or connection dropped. Auto-save solves this: data survives page reload and returns on next visit.
Storage Strategies
LocalStorage — fast, no server, but device and browser bound. Suitable for anonymous forms or drafts.
IndexedDB — same but for large data (files, binaries). Use via idb or localforage.
Server (API) — data available from any device. Requires authorization or session token.
Hybrid — first localStorage (instant), then background sync to server.
LocalStorage Implementation
class FormAutoSave {
constructor(formId, key, debounceMs = 800) {
this.form = document.getElementById(formId);
this.key = key;
this.debounceMs = debounceMs;
this._timer = null;
this._init();
}
_init() {
this._restore();
this.form.addEventListener('input', () => this._schedulesSave());
this.form.addEventListener('change', () => this._schedulesSave());
this.form.addEventListener('submit', () => this.clear());
}
_schedulesSave() {
clearTimeout(this._timer);
this._timer = setTimeout(() => this._save(), this.debounceMs);
}
_save() {
const data = {};
const elements = this.form.elements;
for (const el of elements) {
if (!el.name) continue;
if (el.type === 'checkbox') {
data[el.name] = el.checked;
} else if (el.type === 'radio') {
if (el.checked) data[el.name] = el.value;
} else {
data[el.name] = el.value;
}
}
localStorage.setItem(this.key, JSON.stringify({
data,
savedAt: Date.now(),
}));
this._updateIndicator('saved');
}
_restore() {
const raw = localStorage.getItem(this.key);
if (!raw) return;
const { data, savedAt } = JSON.parse(raw);
const age = Date.now() - savedAt;
if (age > 7 * 24 * 60 * 60 * 1000) { // 7 days — outdated
this.clear();
return;
}
for (const [name, value] of Object.entries(data)) {
const el = this.form.elements[name];
if (!el) continue;
if (el.type === 'checkbox') el.checked = value;
else if (el.type === 'radio') {
const radio = this.form.querySelector(`[name="${name}"][value="${value}"]`);
if (radio) radio.checked = true;
} else {
el.value = value;
}
}
this._updateIndicator('restored');
}
clear() {
localStorage.removeItem(this.key);
}
_updateIndicator(state) {
const el = document.querySelector('[data-autosave-status]');
if (!el) return;
el.textContent = state === 'saved' ? 'Saved' : 'Draft restored';
el.dataset.state = state;
}
}
// Usage
new FormAutoSave('application-form', 'form_draft_application_v1');
Server Sync
When user authorized, draft saved to server:
async function syncDraftToServer(formKey, data) {
try {
await fetch('/api/drafts', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': window.csrfToken,
},
body: JSON.stringify({ key: formKey, data }),
});
} catch {
// Fallback — stay in localStorage, try later
}
}
On Laravel:
// routes/api.php
Route::middleware('auth')->group(function () {
Route::put('/drafts', [DraftController::class, 'store']);
Route::get('/drafts/{key}', [DraftController::class, 'show']);
Route::delete('/drafts/{key}', [DraftController::class, 'destroy']);
});
// DraftController
public function store(Request $request)
{
Draft::updateOrCreate(
['user_id' => auth()->id(), 'key' => $request->key],
['data' => $request->data, 'expires_at' => now()->addDays(30)]
);
return response()->noContent();
}
Status Indicator
User should see when form saved. Three states: "Saving...", "Saved 5 minutes ago", "Save error".
// React variant
function AutoSaveIndicator({ status, savedAt }) {
const labels = {
saving: 'Saving...',
saved: savedAt ? `Saved ${formatRelative(savedAt)}` : 'Saved',
error: 'Save error',
idle: '',
};
return (
<span className={`autosave-badge autosave-badge--${status}`}>
{labels[status]}
</span>
);
}
Multi-Step Forms (wizard)
For wizard forms save not just fields but current step:
const state = {
step: currentStep,
fields: collectFields(),
completedSteps: [1, 2, 3],
};
localStorage.setItem('wizard_draft', JSON.stringify(state));
On restore ask user: "Continue from step 3 or start over?" — modal with two buttons.
Parallel Edit Conflicts
If form open in multiple tabs, use localStorage event for sync:
window.addEventListener('storage', (e) => {
if (e.key === 'form_draft_application_v1') {
const remote = JSON.parse(e.newValue);
if (remote.savedAt > localSavedAt) {
applyRemoteData(remote.data);
}
}
});
Timeframe
Auto-save to localStorage with restore and indicator — 2–3 working days. With server sync, wizard support, conflict resolution, draft history — 5–7 days.







