A form that doesn't forget its draft: Form Schema + Storage Kit
A long form — a multi-step wizard, a lengthy questionnaire — and the user accidentally closes the tab or reloads the page. Without a saved draft, everything typed is gone. Form Schema doesn't keep form state anywhere special (it lives in the reactive values from useForm(), not somewhere else), so wiring up persistence is just watching values and restoring them on init, not rewriting the form's own logic.
The code
<script setup lang="ts">
import { watch } from 'vue'
import { useForm } from '@macrulez/vue-form-schema'
import { useStorage } from 'vue-storage-kit'
import type { FieldDefinition } from '@macrulez/vue-form-schema'
const schema: FieldDefinition[] = [
{ type: 'text', name: 'title', label: 'Title', required: true },
{ type: 'textarea', name: 'body', label: 'Body' },
]
// TTL — don't restore a draft older than a day: the user likely won't
// remember what the form was about anymore, better to start fresh.
const { value: draft } = useStorage('post-draft', {
defaultValue: {} as Record<string, unknown>,
ttl: 24 * 60 * 60 * 1000,
})
const { values, submit } = useForm({
schema,
initialValues: draft.value,
onSubmit: async (data) => {
const response = await fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) })
if (!response.ok) throw new Error(`Failed to save: ${response.status}`)
draft.value = {} // submitted successfully — only now clear the draft
},
})
// deep: true — values is a flat object keyed by schema field, but
// 'group'/'array' fields produce nested structure that shallow watching misses.
watch(values, (v) => (draft.value = { ...v }), { deep: true })
</script>Why watch plus a copy, not a direct draft.value = values assignment
values from useForm() is a Vue-reactive object living inside the composable; draft.value from useStorage() is a separate Ref that gets serialized to storage on every write. Assigning draft.value = values once at init would make draft point at that same reactive object, but useStorage() has no way to know about later mutations of its fields — a storage write happens on .value assignment, not on mutating whatever it points at. A watch with { ...v } on every change is a simple, explicit way to guarantee storage always sees an assignment, not a mutation.
What's next
- Add
debounceto theuseStorage()options — if the draft writes on every keystroke, debounce avoids hittinglocalStorageon every character. See Storage Kit — overview. - For sensitive data,
encrypt: { password }onuseStorage()so the draft doesn't sit inlocalStoragein plain text.