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:

KeyTypeNotes
fieldsFormField[]derived from the io axes — readOnly fields are excluded
fieldsByNameRecord<string, FormField>the same fields, for a form laid out by hand
valuesreactive recordinitial when editing, the field's declared default when creating
errorsreactive recordfield path → message
submit() => Promise<T | null>see cycle below
loadingRef<boolean>
errorRef<FougereError | null>non-validation failure (conflict, dead host)
validComputedRef<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:

FieldAbsentBecause
date()typeneither date nor datetime-local produces the RFC 3339 string the judge expects
bool()requiredon a checkbox it means "must be checked"; the shape says the value must be supplied, and false is one
oneOf(…), bool()typethese 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

  1. Local validationEntity.validate(payload) runs in the browser with the same rules as the handler. Errors are added to errors without a network call, and submit() returns null.
  2. Command — the payload is sent through useCommand(Entity, op). On success, active queries for the entity are revalidated.
  3. Server validation — a returned VALIDATION_FAILED is added to errors by path, using the same format as local errors.
  4. Other errors (CONFLICT, SERVICE_UNAVAILABLE…) are placed in error.

What the form will not do

  • Render components — widgets and layout remain in the application.
  • Show readOnly fields — 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. values uses initial when editing and the field's declared default when creating (oneOf('public','private',{ default:'public' }) opens the select on public). Anything beyond those two — a placeholder, a computed suggestion — is defined by the page.

Next: Session — sharing identity with pages and handlers.

Built with Fougere — this site runs on the framework it documents.