Server Integration & Persistence
Server-side validation errors
Map a backend's validation error response onto form.errors — no hand-rolled unwrapping in every project.
import { applyServerErrors } from '@macrulez/vue-form-schema'
const res = await fetch('/api/users', { method: 'POST', body: JSON.stringify(form.values.value) })
if (!res.ok) {
const { formErrors } = applyServerErrors(form, await res.json(), { format: 'laravel' })
if (formErrors.length) toast.error(formErrors[0]) // errors not tied to a specific field
}Built-in formats:
| Format | Shape |
|---|---|
'laravel' | { message, errors: { field: ["msg", ...], "nested.field": [...] } } |
'drf' | { field: ["msg"], nested: { field: ["msg"] } } (flattened to dot-paths); non_field_errors / detail become formErrors |
'flat' | { field: "msg" | ["msg", ...] } — the default; matches most hand-rolled APIs |
Or pass your own mapper for anything else: (raw) => ({ fieldErrors: {...}, formErrors: [...] }).
Field errors set this way behave like any other entry in errors — the next time that field validates client-side (on blur by default, or on every keystroke with validateOn: 'input'), it's recomputed from the schema's own validators and the server error is naturally replaced. submit() also fully recomputes errors, so a stale server error never survives into the next submit attempt.
applyServerErrors(form, raw, options?):
| Option | Type | Default | Description |
|---|---|---|---|
format | 'laravel' | 'drf' | 'flat' | ((raw) => ...) | 'flat' | Built-in format name or a custom mapper |
touch | boolean | true | Mark affected fields as touched so errors show immediately |
merge | boolean | true | Merge into existing errors instead of replacing them |
normalizeServerErrors(raw, format?) runs the same mapping without touching a form, if you just want { fieldErrors, formErrors } yourself.
Persisted forms
useForm({
schema,
persist: 'local', // or 'session'
persistKey: 'checkout', // optional — defaults to a hash of field names
})Values are restored from storage on onMounted. reset() clears the stored value. SSR-safe: the storage read is guarded by typeof window !== 'undefined'.
Debug mode
// Log every values change to console.group
useForm({ schema, debug: true })// Reactive snapshot of all form state
import { useFormDebug } from '@macrulez/vue-form-schema'
const { snapshot } = useFormDebug(form)
// snapshot.value = { values, errors, touched, isDirty, isValid, isSubmitting }Vue DevTools
vue-form-schema/devtools adds a custom Forms inspector (every active useForm() instance — values/errors/touched/isValid/isDirty, live) and a Forms timeline layer (setField/touch/submit/submitSuccess/submitError/reset/asyncValidate events) to the Vue DevTools browser extension / standalone app.
// main.ts — dev-only, dynamically imported so @vue/devtools-api never
// reaches a production bundle
const app = createApp(App)
if (import.meta.env.DEV) {
const { installFormDevtools } = await import('@macrulez/vue-form-schema/devtools')
installFormDevtools(app)
}
app.mount('#app')This is a separate entry point on purpose — useForm() itself only ever writes to a small dependency-free internal registry (near-zero cost, no @vue/devtools-api import) regardless of whether installFormDevtools is ever called, so devtools support costs nothing in the core bundle unless you opt in. Requires @vue/devtools-api (peer dependency, ^6 || ^7 || ^8 — install it alongside).