Formulaires
useFormFor gère l'état, la validation, la soumission et les erreurs par champ. Il ne rend
aucun composant : la page choisit les champs et leur mise en page.
<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 en cas d'erreur de validation
if (created) navigateTo(`/blog/edit/${created.id}`);
}
</script>
<template>
<form @submit.prevent="onSubmit">
<UFormField label="Titre" :error="errors.title">
<UInput v-model="values.title" v-bind="fieldsByName.title?.attrs" />
</UFormField>
<p v-if="error">{{ error.message }}</p> <!-- échec non-validation -->
<UButton type="submit" :loading="loading" />
</form>
</template>
Signature
useFormFor<T>(EntityOuVue, options?)
// options: {
// op?: string la command du submit — défaut 'create'
// initial?: Record<string, unknown> mode édition : l'entité chargée
// params?: Record<string, string> désignation de la cible — mode édition : { id }
// }
Retour :
| Clé | Type | Notes |
|---|---|---|
fields | FormField[] | dérivés des axes io — les champs readOnly sont exclus |
fieldsByName | Record<string, FormField> | les mêmes champs, pour un formulaire posé à la main |
values | record reactive | initial en édition, le défaut déclaré du champ en création |
errors | record reactive | chemin de champ → message |
submit | () => Promise<T | null> | voir le cycle |
loading | Ref<boolean> | |
error | Ref<FougereError | null> | échec non-validation (conflit, host mort) |
valid | ComputedRef<boolean> | aucune erreur de champ courante |
Le mode édition se compose avec une query :
const { data: post } = await useQuery<Post>(Post, 'findById', { params: { id } });
const form = useFormFor(Post, { op: 'update', params: { id }, initial: post.value ?? undefined });
Le navigateur juge en premier, et gratuitement
field.attrs est la part de la déclaration qu'un navigateur applique déjà, sous les noms
qu'il connaît : type, required, minlength, maxlength, min, max, pattern.
Répandez-la et la page n'énonce aucune règle à elle — text({ min: 1, max: 200 }) devient
minlength/maxlength, email() devient type="email", vérifié en direct pendant la
saisie, sans une ligne de JavaScript.
C'est une projection, pas une seconde règle : le juge lit la même forme. Un formulaire qui
ignore attrs obtient le même verdict, seulement plus tard — et un lecteur d'écran ne
l'obtient jamais.
Trois attributs sont délibérément absents, chacun là où HTML dirait autre chose que la déclaration :
| Champ | Absent | Parce que |
|---|---|---|
date() | type | ni date ni datetime-local ne produit la chaîne RFC 3339 qu'attend le juge |
bool() | required | sur une case à cocher, cela veut dire « doit être cochée » ; la forme dit que la valeur doit être fournie, et false en est une |
oneOf(…), bool() | type | ce ne sont pas des <input> — field.control dit quel widget, la page le rend |
Ce que HTML ne sait pas dire du tout — une règle inter-champs, format: 'uuid', un
oneOf hors d'un select — est attrapé par la passe ci-dessous.
Le cycle du submit
- Validation locale —
Entity.validate(payload)s'exécute dans le navigateur avec les mêmes règles que le handler. Les erreurs sont ajoutées àerrorssans appel réseau etsubmit()renvoienull. - Command — le payload est envoyé par
useCommand(Entity, op). En cas de succès, les queries actives de l'entité sont revalidées. - Validation serveur — un
VALIDATION_FAILEDest ajouté àerrorsselon lepath, dans le même format que les erreurs locales. - Les autres erreurs (
CONFLICT,SERVICE_UNAVAILABLE…) sont placées danserror.
Ce que le formulaire ne fera pas
- Rendre des composants — les widgets et la mise en page restent dans l'application.
- Montrer les champs
readOnly— ces champs sont exclus par la projection io ; un statut s'affiche en badge depuis la donnée chargée, il ne s'édite pas. - Définir les labels — ils restent dans votre i18n ; les métadonnées donnent des clés stables composées de l'entité et du nom du champ.
- Choisir une valeur supplémentaire.
valuesutiliseinitialen édition et le défaut déclaré du champ en création (oneOf('public','private',{ default:'public' })ouvre le select surpublic). Tout ce qui va au-delà de ces deux-là — un placeholder, une suggestion calculée — est défini par la page.
Suite : Session — partager l'identité avec les pages et les handlers.