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éTypeNotes
fieldsFormField[]dérivés des axes io — les champs readOnly sont exclus
fieldsByNameRecord<string, FormField>les mêmes champs, pour un formulaire posé à la main
valuesrecord reactiveinitial en édition, le défaut déclaré du champ en création
errorsrecord reactivechemin de champ → message
submit() => Promise<T | null>voir le cycle
loadingRef<boolean>
errorRef<FougereError | null>échec non-validation (conflit, host mort)
validComputedRef<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 :

ChampAbsentParce que
date()typeni date ni datetime-local ne produit la chaîne RFC 3339 qu'attend le juge
bool()requiredsur 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()typece 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

  1. Validation localeEntity.validate(payload) s'exécute dans le navigateur avec les mêmes règles que le handler. Les erreurs sont ajoutées à errors sans appel réseau et submit() renvoie null.
  2. Command — le payload est envoyé par useCommand(Entity, op). En cas de succès, les queries actives de l'entité sont revalidées.
  3. Validation serveur — un VALIDATION_FAILED est ajouté à errors selon le path, dans le même format que les erreurs locales.
  4. Les autres erreurs (CONFLICT, SERVICE_UNAVAILABLE…) sont placées dans error.

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. values utilise initial en édition et le défaut déclaré du champ en création (oneOf('public','private',{ default:'public' }) ouvre le select sur public). 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.

Construit avec Fougere — ce site tourne sur le framework qu'il documente.