Skip to content

Server Integration & Persistence

Server-side validation errors

Map a backend's validation error response onto form.errors — no hand-rolled unwrapping in every project.

ts
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:

FormatShape
'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?):

OptionTypeDefaultDescription
format'laravel' | 'drf' | 'flat' | ((raw) => ...)'flat'Built-in format name or a custom mapper
touchbooleantrueMark affected fields as touched so errors show immediately
mergebooleantrueMerge 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

ts
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

ts
// Log every values change to console.group
useForm({ schema, debug: true })
ts
// 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.

ts
// 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).