Разработка формы с автосохранением прогресса на сайте
Длинные формы — анкеты, заявки, опросы — теряют пользователей, если те случайно закрыли вкладку или соединение прервалось. Автосохранение решает эту проблему: данные переживают перезагрузку страницы и возвращаются при следующем визите.
Стратегии хранения
LocalStorage — быстро, без сервера, но привязано к устройству и браузеру. Подходит для анонимных форм или черновиков.
IndexedDB — то же самое, но для больших данных (файлы, бинарники). Используем через idb или localforage.
Сервер (API) — данные доступны с любого устройства. Требует авторизации или временного токена сессии.
Гибрид — сначала в localStorage (мгновенно), затем синхронизация с сервером в фоне.
Реализация через localStorage
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 дней — устарело
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' ? 'Сохранено' : 'Черновик восстановлен';
el.dataset.state = state;
}
}
// Использование
new FormAutoSave('application-form', 'form_draft_application_v1');
Синхронизация с сервером
Когда пользователь авторизован, черновик нужно сохранять на сервере:
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 — оставляем в localStorage, попробуем позже
}
}
На 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();
}
Индикатор состояния
Пользователь должен видеть, когда форма сохранена. Три состояния: «Сохраняется...», «Сохранено N минут назад», «Ошибка сохранения».
// React-вариант
function AutoSaveIndicator({ status, savedAt }) {
const labels = {
saving: 'Сохраняется...',
saved: savedAt ? `Сохранено ${formatRelative(savedAt)}` : 'Сохранено',
error: 'Ошибка сохранения',
idle: '',
};
return (
<span className={`autosave-badge autosave-badge--${status}`}>
{labels[status]}
</span>
);
}
Многошаговые формы (wizard)
Для wizard-форм сохраняем не только данные полей, но и текущий шаг:
const state = {
step: currentStep,
fields: collectFields(),
completedSteps: [1, 2, 3],
};
localStorage.setItem('wizard_draft', JSON.stringify(state));
При восстановлении спрашиваем пользователя: «Продолжить с шага 3 или начать заново?» — модальное окно с двумя кнопками.
Конфликты при параллельном редактировании
Если форма открыта в нескольких вкладках, используем localStorage event для синхронизации:
window.addEventListener('storage', (e) => {
if (e.key === 'form_draft_application_v1') {
const remote = JSON.parse(e.newValue);
if (remote.savedAt > localSavedAt) {
applyRemoteData(remote.data);
}
}
});
Сроки
Автосохранение в localStorage с восстановлением и индикатором — 2–3 рабочих дня. С серверной синхронизацией, поддержкой wizard, конфликт-резолюцией и историей черновиков — 5–7 дней.







