Forms
useFormFor manages state, validation, submission, and per-field errors. It renders no
components: the page chooses the fields and their layout.
<script setup lang="ts">
import Post from '@frond/blog/entities/Post';
const { fieldsByName, values, errors, submit, loading, error } = useFormFor<{ id: string }>(Post);
async function onSubmit() {
const created = await submit(); // null on validation error
if (created) navigateTo(`/blog/edit/${created.id}`);
}
</script>
<template>
<form @submit.prevent="onSubmit">
<UFormField label="Title" :error="errors.title">
<UInput v-model="values.title" v-bind="fieldsByName.title?.attrs" />
</UFormField>
<p v-if="error">{{ error.message }}</p> <!-- non-validation failure -->
<UButton type="submit" :loading="loading" />
</form>
</template>
Signature
useFormFor<T>(EntityOrView, options?)
// options: {
// op?: string command the submit rides — default 'create'
// initial?: Record<string, unknown> edit mode: the loaded entity
// params?: Record<string, string> designation of the target — edit mode: { id }
// }
Returns:
| Key | Type | Notes |
|---|---|---|
fields | FormField[] | derived from the io axes — readOnly fields are excluded |
fieldsByName | Record<string, FormField> | the same fields, for a form laid out by hand |
values | reactive record | initial when editing, the field's declared default when creating |
errors | reactive record | field path → message |
submit | () => Promise<T | null> | see cycle below |
loading | Ref<boolean> | |
error | Ref<FougereError | null> | non-validation failure (conflict, dead host) |
valid | ComputedRef<boolean> | no current field errors |
Edit mode composes with a query:
const { data: post } = await useQuery<Post>(Post, 'findById', { params: { id } });
const form = useFormFor(Post, { op: 'update', params: { id }, initial: post.value ?? undefined });
The browser judges first, and for free
field.attrs is the part of the declaration a browser already enforces, under the names
it knows: type, required, minlength, maxlength, min, max, pattern. Spread it
and the page states no rule of its own — text({ min: 1, max: 200 }) becomes
minlength/maxlength, email() becomes type="email", checked live as one types, with
no JavaScript at all.
This is a projection, not a second rule: the judge reads the same shape. A form that
ignores attrs gets the same verdict, only later — and a screen reader never gets it.
Three attributes are deliberately absent, each where HTML would mean something the declaration does not say:
| Field | Absent | Because |
|---|---|---|
date() | type | neither date nor datetime-local produces the RFC 3339 string the judge expects |
bool() | required | on a checkbox it means "must be checked"; the shape says the value must be supplied, and false is one |
oneOf(…), bool() | type | these are not <input>s — field.control says which widget, the page renders it |
What HTML cannot say at all — a cross-field rule, format: 'uuid', a oneOf outside a
select — is caught by the pass below.
The submit cycle
- Local validation —
Entity.validate(payload)runs in the browser with the same rules as the handler. Errors are added toerrorswithout a network call, andsubmit()returnsnull. - Command — the payload is sent through
useCommand(Entity, op). On success, active queries for the entity are revalidated. - Server validation — a returned
VALIDATION_FAILEDis added toerrorsbypath, using the same format as local errors. - Other errors (
CONFLICT,SERVICE_UNAVAILABLE…) are placed inerror.
What the form will not do
- Render components — widgets and layout remain in the application.
- Show
readOnlyfields — these fields are excluded by the io projection; a status is displayed as a badge from the loaded data, not editable as an input. - Define labels — they remain in your i18n; the metadata provides stable keys composed of the entity and field name.
- Choose an additional value.
valuesusesinitialwhen editing and the field's declared default when creating (oneOf('public','private',{ default:'public' })opens the select onpublic). Anything beyond those two — a placeholder, a computed suggestion — is defined by the page.
Next: Session — sharing identity with pages and handlers.