Draft persistence
Fill in a few fields — including some checkboxes — then reload the page. The draft comes back, checkbox selections included. Submitting or discarding clears it.
What's going on here
Every field spreads .persist, which is the entire opt-in: values save to local
storage as you type (debounced, flushed when a value commits) and restore when the inputs
mount. Restored fields are marked dirty, since a restored draft is user input rather than
pristine state. persist.maxAgeMs here drops drafts older than seven days.
The draft clears itself on a successful submission, and the "Discard draft" button calls form.reset() — which resets the form element, the validation state, and the saved
draft in one go.
<script lang="ts">
import { enhancedForm } from '@opensky/remotes'
import { applicationForm } from './form.remote'
import { applicationSchema } from './schema'
const skills = ['Svelte', 'TypeScript', 'Design systems', 'Databases']
// Every field below spreads `.persist`, so the whole application survives
// a reload. The draft is dropped on submit, on discard, or after 7 days.
const form = enhancedForm(applicationForm, {
delayMs: 300,
timeoutMs: 5000,
persist: { maxAgeMs: 7 * 24 * 60 * 60 * 1000 }
})
</script>
<form
class="demo"
{...form.handlers}
{...applicationForm.preflight(applicationSchema).enhance((instance) =>
form.enhance(instance, {
onReturn: () => console.log('application submitted')
})
)}
>
<label>
Full name
<input
{...applicationForm.fields.name.as('text')}
{...form.fields.name.validate}
{...form.fields.name.persist}
autocomplete="name"
class:invalid={form.fields.name.issues}
/>
</label>
{#if form.fields.name.issues}
{#each form.fields.name.issues as issue (issue)}
<p class="error">{issue}</p>
{/each}
{/if}
<label>
Email
<input
{...applicationForm.fields.email.as('email')}
{...form.fields.email.validate}
{...form.fields.email.persist}
autocomplete="email"
class:invalid={form.fields.email.issues}
/>
</label>
{#if form.fields.email.issues}
{#each form.fields.email.issues as issue (issue)}
<p class="error">{issue}</p>
{/each}
{/if}
<label>
Role
<select
{...applicationForm.fields.role.as('select')}
{...form.fields.role.validate}
{...form.fields.role.persist}
class:invalid={form.fields.role.issues}
>
<option value="">Choose a role…</option>
<option value="engineer">Engineer</option>
<option value="designer">Designer</option>
<option value="product">Product manager</option>
</select>
</label>
{#if form.fields.role.issues}
{#each form.fields.role.issues as issue (issue)}
<p class="error">{issue}</p>
{/each}
{/if}
<fieldset>
<legend>Skills</legend>
{#each skills as skill (skill)}
<label class="checkbox">
<input
{...applicationForm.fields.skills.as('checkbox', skill)}
{...form.fields.skills.validate}
{...form.fields.skills.persist}
/>
{skill}
</label>
{/each}
</fieldset>
{#if form.fields.skills.issues}
{#each form.fields.skills.issues as issue (issue)}
<p class="error">{issue}</p>
{/each}
{/if}
<label>
Cover letter
<textarea
{...applicationForm.fields.coverLetter.as('text')}
{...form.fields.coverLetter.validate}
{...form.fields.coverLetter.persist}
rows="6"
placeholder="Why do you want to work with us?"
class:invalid={form.fields.coverLetter.issues}
></textarea>
</label>
{#if form.fields.coverLetter.issues}
{#each form.fields.coverLetter.issues as issue (issue)}
<p class="error">{issue}</p>
{/each}
{/if}
<div class="actions">
<button type="submit" class="press press-dark" disabled={form.pending}>
{form.delayed ? 'Submitting…' : 'Submit application'}
</button>
<!-- reset() clears the form, the validation state, and the saved draft -->
<button type="button" class="press press-ghost" onclick={() => form.reset()}>
Discard draft
</button>
</div>
{#if form.result}
<p class="success">Application submitted — the draft has been cleared.</p>
{/if}
{#if form.timeout}
<p class="pending">The request is taking longer than expected.</p>
{/if}
</form>import { form } from '$app/server'
import { applicationSchema } from './schema'
export const applicationForm = form(applicationSchema, async ({ name, email, role, skills }) => {
console.log('Application received:', name, email, role, skills)
// Simulate processing so the 'delayed' state is visible
await new Promise((resolve) => setTimeout(resolve, 900))
return {
success: true
}
})import * as v from 'valibot'
export const applicationSchema = v.object({
name: v.pipe(v.string(), v.minLength(2, 'Enter your full name')),
email: v.pipe(v.string(), v.email('Enter a valid email')),
role: v.pipe(v.string(), v.minLength(1, 'Pick a role')),
skills: v.pipe(v.array(v.string()), v.minLength(1, 'Pick at least one skill')),
coverLetter: v.pipe(v.string(), v.minLength(40, 'Tell us a bit more — at least 40 characters'))
})