Перейти к основному содержимому

Form Guardian для Svelte

Полное руководство по использованию Form Guardian с Svelte и SvelteKit приложениями.

🚀 Быстрый старт

Установка

npm install @form-guardian/dom

Базовое использование

<script>
import { onMount, onDestroy } from 'svelte';
import { attachFormAutosave } from '@form-guardian/dom';

let formElement;
let autosave;

onMount(() => {
autosave = attachFormAutosave({
formId: 'contact-form',
root: formElement,
autoRestore: true,
debounceMs: 500,
});
});

onDestroy(() => {
if (autosave) {
autosave.destroy();
}
});

async function handleSubmit(event) {
event.preventDefault();
await autosave.clear();
// Логика отправки...
}
</script>

<form bind:this={formElement} on:submit={handleSubmit}>
<input name="name" placeholder="Имя" />
<input name="email" type="email" placeholder="Email" />
<textarea name="message" placeholder="Сообщение" />
<button type="submit">Отправить</button>
</form>

📋 Полные примеры

Со статусом черновика

<script>
import { onMount, onDestroy } from 'svelte';
import { attachFormAutosave } from '@form-guardian/dom';

let formElement;
let autosave;
let hasDraft = false;
let draftTime = null;

onMount(async () => {
autosave = attachFormAutosave({
formId: 'contact-form',
root: formElement,
autoRestore: true,
onAfterSave: async () => {
hasDraft = true;
draftTime = new Date();
},
});

// Проверка наличия существующего черновика
hasDraft = await autosave.hasDraft();
if (hasDraft) {
const meta = await autosave.getDraftMeta();
draftTime = new Date(meta.updatedAt);
}
});

onDestroy(() => {
autosave?.destroy();
});

async function handleSubmit(event) {
event.preventDefault();
await autosave.clear();
hasDraft = false;
draftTime = null;
// Отправка...
}
</script>

{#if hasDraft}
<div class="draft-alert">
📝 Черновик сохранен в {draftTime?.toLocaleTimeString()}
</div>
{/if}

<form bind:this={formElement} on:submit={handleSubmit}>
<input name="name" placeholder="Имя" />
<input name="email" type="email" placeholder="Email" />
<textarea name="message" placeholder="Сообщение" />
<button type="submit">Отправить</button>
</form>

<style>
.draft-alert {
padding: 8px 16px;
background: #e3f2fd;
border-radius: 4px;
margin-bottom: 16px;
}
</style>

С диалогом ручного восстановления

<script>
import { onMount, onDestroy } from 'svelte';
import { attachFormAutosave } from '@form-guardian/dom';

let formElement;
let autosave;
let showRestoreDialog = false;
let draftTimestamp = null;

onMount(async () => {
autosave = attachFormAutosave({
formId: 'my-form',
root: formElement,
autoRestore: false, // Ручное восстановление
});

const hasDraft = await autosave.hasDraft();
if (hasDraft) {
const meta = await autosave.getDraftMeta();
draftTimestamp = meta.updatedAt;
showRestoreDialog = true;
}
});

onDestroy(() => {
autosave?.destroy();
});

async function restoreDraft() {
await autosave.restore();
showRestoreDialog = false;
}

async function discardDraft() {
await autosave.clear();
showRestoreDialog = false;
}
</script>

{#if showRestoreDialog}
<div class="modal">
<div class="modal-content">
<h3>Найден несохраненный черновик</h3>
<p>Последнее сохранение: {new Date(draftTimestamp).toLocaleString()}</p>
<button on:click={restoreDraft}>Восстановить</button>
<button on:click={discardDraft}>Отказаться</button>
</div>
</div>
{/if}

<form bind:this={formElement}>
<!-- поля формы -->
</form>

С валидацией формы

<script>
import { onMount, onDestroy } from 'svelte';
import { attachFormAutosave } from '@form-guardian/dom';

let formElement;
let autosave;

let formData = {
name: '',
email: '',
message: ''
};

let errors = {};

onMount(() => {
autosave = attachFormAutosave({
formId: 'contact-form',
root: formElement,
autoRestore: true,
onAfterRestore: () => {
// Запуск валидации после восстановления
validateForm();
},
});
});

onDestroy(() => {
autosave?.destroy();
});

function validateForm() {
errors = {};

if (!formData.name || formData.name.length < 2) {
errors.name = 'Имя должно быть не менее 2 символов';
}

if (!formData.email || !formData.email.includes('@')) {
errors.email = 'Неверный email адрес';
}

if (!formData.message) {
errors.message = 'Сообщение обязательно';
}

return Object.keys(errors).length === 0;
}

async function handleSubmit(event) {
event.preventDefault();

if (!validateForm()) {
return;
}

await autosave.clear();
// Отправка формы...
}
</script>

<form bind:this={formElement} on:submit={handleSubmit}>
<div>
<label for="name">Имя</label>
<input
id="name"
name="name"
bind:value={formData.name}
on:blur={validateForm}
/>
{#if errors.name}
<span class="error">{errors.name}</span>
{/if}
</div>

<div>
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
bind:value={formData.email}
on:blur={validateForm}
/>
{#if errors.email}
<span class="error">{errors.email}</span>
{/if}
</div>

<div>
<label for="message">Сообщение</label>
<textarea
id="message"
name="message"
bind:value={formData.message}
on:blur={validateForm}
/>
{#if errors.message}
<span class="error">{errors.message}</span>
{/if}
</div>

<button type="submit">Отправить</button>
</form>

<style>
.error {
color: red;
font-size: 0.875rem;
}
</style>

🔧 Продвинутые паттерны

Переиспользуемый Store

Создайте переиспользуемый Svelte store для автосохранения:

// stores/autosave.js
import { writable } from 'svelte/store';
import { attachFormAutosave } from '@form-guardian/dom';

export function createAutosaveStore(formId) {
const { subscribe, set, update } = writable({
hasDraft: false,
timestamp: null,
saving: false,
});

let autosave = null;

return {
subscribe,
init: async (formElement, options = {}) => {
autosave = attachFormAutosave({
formId,
root: formElement,
...options,
onBeforeSave: async (values) => {
update(state => ({ ...state, saving: true }));
await options.onBeforeSave?.(values);
},
onAfterSave: async (values) => {
update(state => ({
hasDraft: true,
timestamp: Date.now(),
saving: false,
}));
await options.onAfterSave?.(values);
},
});

const hasDraft = await autosave.hasDraft();
if (hasDraft) {
const meta = await autosave.getDraftMeta();
set({
hasDraft: true,
timestamp: meta.updatedAt,
saving: false,
});
}
},
clear: async () => {
await autosave?.clear();
set({ hasDraft: false, timestamp: null, saving: false });
},
destroy: () => {
autosave?.destroy();
},
};
}

Использование:

<script>
import { onMount, onDestroy } from 'svelte';
import { createAutosaveStore } from './stores/autosave';

let formElement;
const autosave = createAutosaveStore('my-form');

onMount(() => {
autosave.init(formElement, {
autoRestore: true,
debounceMs: 500,
});
});

onDestroy(() => {
autosave.destroy();
});

async function handleSubmit(event) {
event.preventDefault();
await autosave.clear();
// Отправка...
}
</script>

<div>
{#if $autosave.hasDraft}
<div class="draft-status">
{#if $autosave.saving}
💾 Сохранение...
{:else}
✅ Черновик сохранен
{/if}
</div>
{/if}

<form bind:this={formElement} on:submit={handleSubmit}>
<!-- поля -->
</form>
</div>

Интеграция с SvelteKit Form Actions

<!-- +page.svelte -->
<script>
import { onMount, onDestroy } from 'svelte';
import { enhance } from '$app/forms';
import { attachFormAutosave } from '@form-guardian/dom';

let formElement;
let autosave;

onMount(() => {
autosave = attachFormAutosave({
formId: 'contact-form',
root: formElement,
autoRestore: true,
});
});

onDestroy(() => {
autosave?.destroy();
});

async function handleEnhance() {
return async ({ result }) => {
if (result.type === 'success') {
// Очистка черновика при успешной отправке
await autosave.clear();
}
};
}
</script>

<form
bind:this={formElement}
method="POST"
use:enhance={handleEnhance}
>
<input name="name" placeholder="Имя" />
<input name="email" type="email" placeholder="Email" />
<textarea name="message" placeholder="Сообщение" />
<button type="submit">Отправить</button>
</form>
// +page.server.js
export const actions = {
default: async ({ request }) => {
const data = await request.formData();

// Обработка данных формы
const name = data.get('name');
const email = data.get('email');
const message = data.get('message');

// Отправка email, сохранение в БД и т.д.

return { success: true };
}
};

Многошаговая форма с Svelte Stores

// stores/wizard.js
import { writable } from 'svelte/store';
import { saveDraftCore, loadDraftCore, clearDraft } from '@form-guardian/core';

function createWizardStore() {
const { subscribe, set, update } = writable({
currentStep: 0,
steps: [],
});

return {
subscribe,
registerStep: (stepId) => {
update(state => ({
...state,
steps: [...state.steps, stepId],
}));
},
nextStep: () => {
update(state => ({
...state,
currentStep: Math.min(state.currentStep + 1, state.steps.length - 1),
}));
},
prevStep: () => {
update(state => ({
...state,
currentStep: Math.max(state.currentStep - 1, 0),
}));
},
saveStep: async (stepId, data) => {
await saveDraftCore(stepId, data);
},
loadStep: async (stepId) => {
const draft = await loadDraftCore(stepId);
return draft?.values;
},
clearAll: async () => {
const state = writable({});
subscribe(state.set);
const { steps } = state;
for (const stepId of steps) {
await clearDraft(stepId);
}
},
};
}

export const wizard = createWizardStore();

💡 Лучшие практики

✅ ДЕЛАЙТЕ

  • Всегда вызывайте destroy() в onDestroy
  • Используйте autoRestore: true для лучшего UX
  • Очищайте черновик после успешной отправки
  • Показывайте статус черновика пользователям
  • Используйте Svelte stores для общего состояния

❌ НЕ ДЕЛАЙТЕ

  • Не забывайте делать очистку (вызывать destroy())
  • Не сохраняйте поля паролей (уже исключены по умолчанию)
  • Не сохраняйте при каждом нажатии клавиши без debounce
  • Не привязывайтесь к элементам формы до монтирования

🔗 Связанные ресурсы