Skip to main content

Form Guardian for Svelte

Complete guide to using Form Guardian with Svelte and SvelteKit applications.

🚀 Quick Start

Installation

npm install @form-guardian/dom

Basic Usage

<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();
// Submit logic...
}
</script>

<form bind:this={formElement} on:submit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<textarea name="message" placeholder="Message" />
<button type="submit">Send</button>
</form>

📋 Complete Examples

With Draft Status

<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();
},
});

// Check for existing draft
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;
// Submit...
}
</script>

{#if hasDraft}
<div class="draft-alert">
📝 Draft saved at {draftTime?.toLocaleTimeString()}
</div>
{/if}

<form bind:this={formElement} on:submit={handleSubmit}>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<textarea name="message" placeholder="Message" />
<button type="submit">Send</button>
</form>

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

With Manual Restore Dialog

<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, // Manual restore
});

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>Unsaved Draft Found</h3>
<p>Last saved: {new Date(draftTimestamp).toLocaleString()}</p>
<button on:click={restoreDraft}>Restore</button>
<button on:click={discardDraft}>Discard</button>
</div>
</div>
{/if}

<form bind:this={formElement}>
<!-- form fields -->
</form>

With Form Validation

<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: () => {
// Trigger validation after restore
validateForm();
},
});
});

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

function validateForm() {
errors = {};

if (!formData.name || formData.name.length < 2) {
errors.name = 'Name must be at least 2 characters';
}

if (!formData.email || !formData.email.includes('@')) {
errors.email = 'Invalid email address';
}

if (!formData.message) {
errors.message = 'Message is required';
}

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

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

if (!validateForm()) {
return;
}

await autosave.clear();
// Submit form...
}
</script>

<form bind:this={formElement} on:submit={handleSubmit}>
<div>
<label for="name">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">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">Submit</button>
</form>

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

🔧 Advanced Patterns

Reusable Store

Create a reusable Svelte store for autosave:

// 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();
},
};
}

Usage:

<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();
// Submit...
}
</script>

<div>
{#if $autosave.hasDraft}
<div class="draft-status">
{#if $autosave.saving}
💾 Saving...
{:else}
✅ Draft saved
{/if}
</div>
{/if}

<form bind:this={formElement} on:submit={handleSubmit}>
<!-- fields -->
</form>
</div>

SvelteKit Form Actions Integration

<!-- +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') {
// Clear draft on successful submission
await autosave.clear();
}
};
}
</script>

<form
bind:this={formElement}
method="POST"
use:enhance={handleEnhance}
>
<input name="name" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<textarea name="message" placeholder="Message" />
<button type="submit">Send</button>
</form>
// +page.server.js
export const actions = {
default: async ({ request }) => {
const data = await request.formData();

// Process form data
const name = data.get('name');
const email = data.get('email');
const message = data.get('message');

// Send email, save to database, etc.

return { success: true };
}
};

Multi-Step Form with 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();

💡 Best Practices

✅ DO

  • Always call destroy() in onDestroy
  • Use autoRestore: true for better UX
  • Clear draft after successful submission
  • Show draft status to users
  • Use Svelte stores for shared state

❌ DON'T

  • Don't forget to cleanup (call destroy())
  • Don't save password fields (already excluded by default)
  • Don't save on every keystroke without debounce
  • Don't bind to form elements before mounting