useFormDraft
Auto-saves a form in progress to localStorage on an interval, and hands it back when the reader returns.
vue
<script setup lang="ts">
import { useFormDraft } from '@core/schema/useFormDraft'
import { Button } from '@shared/components/ui/button'
import { Input } from '@shared/components/ui/input'
import { Label } from '@shared/components/ui/label'
import { Textarea } from '@shared/components/ui/textarea'
import { reactive, ref } from 'vue'
interface Article {
title: string
body: string
}
const form = reactive<Article>({ title: '', body: '' })
const status = ref('No draft saved yet.')
const { restore, save, clearDraft } = useFormDraft<Article>(
'docs-article-demo',
() => ({ ...form }),
{ intervalMs: 5000 },
)
function saveNow(): void {
save()
status.value = `Saved at ${new Date().toLocaleTimeString()}.`
}
function restoreNow(): void {
const draft = restore()
if (draft) {
Object.assign(form, draft)
status.value = 'Restored from the saved draft.'
}
else {
status.value = 'Nothing saved.'
}
}
function clearNow(): void {
clearDraft()
form.title = ''
form.body = ''
status.value = 'Draft cleared.'
}
</script>
<template>
<div class="flex flex-col gap-4">
<div class="grid gap-2">
<Label for="draft-demo-title">Title</Label>
<Input id="draft-demo-title" v-model="form.title" placeholder="Quarterly report" />
</div>
<div class="grid gap-2">
<Label for="draft-demo-body">Body</Label>
<Textarea id="draft-demo-body" v-model="form.body" rows="3" placeholder="Type something, then save and clear." />
</div>
<div class="flex flex-wrap gap-2">
<Button size="sm" @click="saveNow()">
Save now
</Button>
<Button size="sm" variant="outline" @click="restoreNow()">
Restore
</Button>
<Button size="sm" variant="outline" @click="clearNow()">
Clear
</Button>
</div>
<p class="sa-text-sm text-muted-foreground">
{{ status }}
</p>
</div>
</template>Import
ts
import { useFormDraft } from '@core/schema/useFormDraft'Signature
ts
function useFormDraft<T>(
key: string,
getData: () => T,
options?: UseFormDraftOptions,
): UseFormDraftReturn<T>
interface UseFormDraftOptions {
intervalMs?: number
enabled?: MaybeRefOrGetter<boolean>
}Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key | string | required | Stored under sa-draft:{key}. |
getData | () => T | required | Called on every save. A getter, not a ref, because vee-validate's values is a proxy. |
options.intervalMs | number | 30000 | How often the interval saves. |
options.enabled | MaybeRefOrGetter<boolean> | true | Reactive. While false the interval keeps running but writes nothing. |
Returns
| Member | Type | Description |
|---|---|---|
restore | () => T | null | Reads the draft synchronously. null when missing or corrupt. |
save | () => void | Saves now, in addition to the interval. |
clearDraft | () => void | Removes the draft. Call it after a successful submit. |
Example
vue
<script setup lang="ts">
import { useFormDraft } from '@core/schema/useFormDraft'
import { reactive } from 'vue'
interface Article {
title: string
body: string
}
const form = reactive<Article>({ title: '', body: '' })
const { restore, save, clearDraft } = useFormDraft<Article>(
'article-new',
() => ({ ...form }),
{ intervalMs: 10_000 },
)
const draft = restore()
if (draft)
Object.assign(form, draft)
async function submit() {
await fetch('/api/articles', { method: 'POST', body: JSON.stringify(form) })
clearDraft()
}
</script>
<template>
<form class="space-y-3" @submit.prevent="submit()">
<input v-model="form.title" type="text" placeholder="Title">
<textarea v-model="form.body" rows="5" />
<div class="flex gap-2">
<button type="submit">
Publish
</button>
<button type="button" @click="save()">
Save draft now
</button>
</div>
</form>
</template>Notes
- Restore is manual, and that is deliberate.
SchemaFormrestores before it callsuseForm(), so the draft seedsinitialValuesat first paint rather than overwriting fields a tick later. AnonMountedwrite would flash the empty form first. getDatais a getter rather than a ref for the same reason: vee-validate exposesvaluesas a proxy, not a ref, so there is nothing to watch.- Nothing here throws. A failed read, write or clear is caught and logged through
console.warn, so a full or blockedlocalStoragecannot break a form. - The interval is paused on scope dispose, so a form that unmounts stops saving.
- A corrupt entry reads as
null, which is indistinguishable from "no draft" to the caller. That is the safe default. - Keys are namespaced
sa-draft:so clearing drafts never touches the rest of the app's storage.
Related
useFormDirty— the other half: warning before a reader leaves.- Forms — where
SchemaFormcalls this.