Yayaw
Documentation
Modifier et agir

Formulaires et validation

Relier champs, valeurs initiales, validation et soumission partielle.

Un catalogue de formulaire décrit l’édition d’une fiche. Donnez-lui un identifiant stable, définissez les champs puis reliez-le au type de formulaire de création ou d’édition du tableau.

Utilisez TableConfig.presentation à la racine pour une surface commune de consultation, création, édition et édition groupée, avec un choix mobile facultatif. Le défaut est un drawer à droite sur tous les écrans. Consultez la présentation commune des fiches pour la configuration et la migration.

Exemple de configuration

Partez du démarrage React ou du démarrage Vue. Le fichier suivant complète leur catalogue de produits. Passez cette configuration via getTableConfig en React ou config en Vue.

product-form.ts
import { z } from "zod";
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";

export const productForm: FormConfig = {
  id: "products",
  title: "Edit product",
  presentation: "drawer",
  width: "38rem",
  submitMode: "patch",
  fields: [
    {
      name: "name",
      label: "Name",
      type: "text",
      required: true,
      bulkEdit: false,
      schema: z.string().trim().min(1),
    },
    {
      name: "price",
      label: "Price",
      type: "number",
      min: 0,
      schema: z.number().min(0),
    },
    {
      name: "stock",
      label: "Stock",
      type: "number",
      min: 0,
      step: 1,
      schema: z.number().int().min(0),
    },
    {
      name: "status",
      label: "Status",
      type: "select",
      options: [
        { value: "draft", label: "Draft" },
        { value: "active", label: "Active" },
        { value: "archived", label: "Archived" },
      ],
    },
  ],
  blocks: [
    {
      type: "content",
      id: "help",
      text: "Only changed fields are saved.",
      tone: "info",
    },
    { type: "field", name: "name" },
    {
      type: "section",
      id: "inventory",
      title: "Inventory",
      columns: 2,
      blocks: [
        { type: "field", name: "price" },
        { type: "field", name: "stock" },
      ],
    },
    { type: "field", name: "status" },
  ],
};
product-form.ts
import { z } from "zod";
import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";

export const productForm: FormConfig = {
  id: "products",
  title: "Edit product",
  presentation: "drawer",
  width: "38rem",
  submitMode: "patch",
  fields: [
    {
      name: "name",
      label: "Name",
      type: "text",
      required: true,
      bulkEdit: false,
      schema: z.string().trim().min(1),
    },
    {
      name: "price",
      label: "Price",
      type: "number",
      min: 0,
      schema: z.number().min(0),
    },
    {
      name: "stock",
      label: "Stock",
      type: "number",
      min: 0,
      step: 1,
      schema: z.number().int().min(0),
    },
    {
      name: "status",
      label: "Status",
      type: "select",
      options: [
        { value: "draft", label: "Draft" },
        { value: "active", label: "Active" },
        { value: "archived", label: "Archived" },
      ],
    },
  ],
  blocks: [
    {
      type: "content",
      id: "help",
      text: "Only changed fields are saved.",
      tone: "info",
    },
    { type: "field", name: "name" },
    {
      type: "section",
      id: "inventory",
      title: "Inventory",
      columns: 2,
      blocks: [
        { type: "field", name: "price" },
        { type: "field", name: "stock" },
      ],
    },
    { type: "field", name: "status" },
  ],
};
get-form-config.ts
import type {
  FieldValues,
  FormConfig,
} from "@/components/ui/yayaw-table/components/forms/types";
import { productForm } from "./product-form";

/** The registry's generic lookup boundary resolves this known product catalogue. */
export function getFormConfig<T extends FieldValues>(
  formType: string
): FormConfig<T> | undefined {
  const form = formType === "products" ? productForm : undefined;
  const resolved = form;
  return resolved as FormConfig<T> | undefined;
}
get-form-config.ts
import { productForm } from "./product-form";
export const getFormConfig = (formType: string) =>
  formType === "products" ? productForm : undefined;

Brancher le catalogue

Définissez form.editFormType: "products" dans le catalogue du tableau et passez getFormConfig au composant. Fournissez update via le catalogue d’actions. La recette administration branche les trois. Pour créer, utilisez createFormType, des valeurs initiales et une action create renvoyant la fiche enregistrée.

Initialiser, valider, transformer

defaultValues initialise la création. loadInitialValues peut hydrater une fiche de manière asynchrone avec un signal d’annulation. Les schémas de champ valident chaque contrôle ; le schéma de formulaire valide l’ensemble. transform adapte les valeurs au contrat d’écriture. submitMode: "patch" transmet les champs déclarés modifiés ; la soumission complète reste le comportement par défaut.

Champs conditionnels

FormConfig.rules affiche, masque, rend obligatoires ou renseigne des champs d’après d’autres valeurs, dans les formulaires de création, de modification et de modification groupée. Les règles ont la même forme et les mêmes comparaisons que les questions conditionnelles de la vue Formulaire : les conditions lisent des noms de champs (fieldId), et les effets listent les champs visés dans then.fieldIds. Les règles des formulaires de fiche se configurent en code ; aucun panneau de réglages ne les édite.

product-form-rules.ts
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
// Vue : import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
import { productForm } from "./product-form";

const statusIs = (value: string) => ({
  join: "and" as const,
  items: [{ fieldId: "status", operator: "is" as const, value }],
});

export const exampleForm: FormConfig = {
  ...productForm,
  rules: [
    // Un produit actif doit avoir un prix.
    { id: "active-price", when: statusIs("active"), then: { action: "require", fieldIds: ["price"] } },
    // Un produit archivé ne suit plus son stock.
    { id: "archived-stock", when: statusIs("archived"), then: { action: "hide", fieldIds: ["stock"] } },
  ],
};
  • show masque ses champs tant qu’aucune de ses règles ne correspond ; hide l’emporte sur show.

  • require rend obligatoire un champ affiché, en plus de son propre indicateur required et de son schéma.

  • set ({ action: "set", fieldIds, value }) écrit une valeur avant la validation du formulaire.

  • Les champs masqués ne sont ni validés ni envoyés, en soumission complète comme en mode patch, et l’édition en cellule refuse un champ que ses règles masquent. Une valeur masquée compte comme vide pour les autres conditions.

  • Une erreur déjà affichée sous un champ est revérifiée quand les valeurs changent : une règle qui masque un champ ou ne l’exige plus ne laisse jamais une erreur périmée bloquer Enregistrer.

  • Les champs des éléments de collection n’héritent pas des règles du formulaire parent.

  • Les règles qui ne peuvent pas agir, comme une condition sur un champ inconnu ou une comparaison qui ne convient pas à son type, sont écartées au chargement du formulaire.

Comparaisons par type de champ

type du champComparé commeComparaisons (operator)
text, textarea et autres champs texteTexteis, isNot, contains, notContains, startsWith
number, currency, percentNombreeq, neq, lt, lte, gt, gte, between
date, datetimeDate, au jour prèson, before, after, between, inLast, inNext (un nombre de jours)
select, radio, select-with-add-new, tagChoix uniqueis, isNot, isAnyOf, isNoneOf
multiSelect, tagsChoix multiplecontainsAny, containsAll, containsNone
boolean, checkbox, switchOui/nonisChecked, isUnchecked

Tous les types sauf oui/non acceptent aussi isEmpty et isNotEmpty. between prend [from, to], dont une borne peut valoir null ; les comparaisons de liste prennent un tableau de valeurs d’options.

Modification groupée et valeurs mixtes

En modification groupée, les conditions lisent la valeur saisie dans le brouillon, ou à défaut la valeur commune à toutes les lignes sélectionnées. Quand les lignes sélectionnées ont des valeurs différentes pour un champ absent du brouillon, ce champ est « mixte » et toute condition qui le lit est considérée comme non remplie :

  • Un champ masqué uniquement à cause de valeurs mixtes est listé, désactivé, dans Ajouter un champ, avec une note comme « Dépend de Catégorie, dont les valeurs diffèrent entre les lignes. Modifiez d’abord Catégorie. ».

  • Un champ déjà ajouté dont les règles lisent des valeurs mixtes affiche dessous « Les valeurs de Catégorie diffèrent entre les lignes : la condition est considérée comme non remplie. ».

Ajouter le champ mixte au brouillon lui donne une même valeur pour toutes les lignes, et les conditions lisent alors cette valeur. Les notes ont un texte intégré en anglais et en français.

Migrer depuis l’option hidden

Les champs gardent leur option hidden, true ou une fonction (context) => boolean. Au chargement du formulaire, elle est convertie en règle avec une condition réservée au code, qui reçoit le contexte complet et les valeurs telles quelles : les formulaires existants se comportent comme avant. Ces conditions en code ne sont jamais enregistrées en JSON. Le code qui lit hidden hors d’un formulaire, comme un rendu de cellule, continue d’appeler la fonction directement.

Une fonction hidden qui ne lit que d’autres valeurs peut devenir une règle :

// Avant : un prédicat sur le champ.
{ name: "stock", label: "Stock", type: "number", hidden: ({ values }) => values?.status === "archived" }

// Après : une règle déclarative sur le formulaire.
rules: [
  {
    id: "archived-stock",
    when: { join: "and", items: [{ fieldId: "status", operator: "is", value: "archived" }] },
    then: { action: "hide", fieldIds: ["stock"] },
  },
],

Les règles déclaratives valent la migration : elles comptent les valeurs masquées comme vides, si bien que les enchaînements se stabilisent ; elles peuvent rendre obligatoires ou renseigner des champs ; et en modification groupée elles lisent les valeurs communes à la sélection et expliquent les valeurs mixtes, alors qu’un ancien prédicat continue de s’exécuter ligne par ligne. Gardez une fonction quand la décision dépend d’autre chose que des valeurs, comme la ligne, le mode ou l’utilisateur.

Choisir la suite

Continuez avec la composition, les options asynchrones, les sélecteurs par tableau, les collections, l’édition en cellule ou l’édition groupée. La référence conserve les contrats détaillés et les exemples de champs.

Sections du guide précédent

Formulaires, sections, multi-select et champs collection

Lire la référence détaillée.

Contrat de formulaire commun à React et Vue

Lire la référence détaillée.

Sélecteur avec table

Lire la référence détaillée.

Initialisation, options et envoi

Lire la référence détaillée.

Champs multi-select

Lire la référence détaillée.

Sections de formulaire

Lire la référence détaillée.

Ce que permettent les champs collection

Lire la référence détaillée.

Modèle mental

Lire la référence détaillée.

Exemple rapide

Lire la référence détaillée.

Référence API

Lire la référence détaillée.

Plusieurs types d'items

Lire la référence détaillée.

Collections imbriquées

Lire la référence détaillée.

Couches de validation

Lire la référence détaillée.

Formulaires en modal et drawer

Lire la référence détaillée.

Traduction et labels

Lire la référence détaillée.

Quand garder custom

Lire la référence détaillée.

Pièges fréquents

Lire la référence détaillée.

Docs liées

Lire la référence détaillée.

Champs générés et JSON

Lire la référence détaillée.

Form blocks

Lire la référence détaillée.

Consultation d’une entrée

Lire la référence détaillée.

Historique conservé et annulation

Lire la référence détaillée.

Tester les comportements dans l’exemple Yayaw

Lire la référence détaillée.

Une modale compacte à deux champs

Renvoyez exampleForm depuis le résolveur getFormConfig existant pour products ; conservez la même action update. Chaque variante ci-dessous est un fichier complet qui étend le formulaire produit présenté plus haut.

modal-product-form.ts
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
import { productForm } from "./product-form";

export const exampleForm: FormConfig = {
  ...productForm,
  presentation: "modal",
  width: "32rem",
  blocks: undefined,
  fields: productForm.fields.filter((field) =>
    ["name", "price"].includes(field.name)
  ),
};
modal-product-form.ts
import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
import { productForm } from "./product-form";

export const exampleForm: FormConfig = {
  ...productForm,
  presentation: "modal",
  width: "32rem",
  blocks: undefined,
  fields: productForm.fields.filter((field) =>
    ["name", "price"].includes(field.name)
  ),
};

Exiger une valeur de stock explicite

Conservez le schéma entier et positif ou nul, et exigez une valeur. Zéro est valide ; un champ vide ou une quantité fractionnaire ne le sont pas. Un échec de l’action conserve le brouillon.

required-stock-form.ts
import type { FormConfig } from "@/components/ui/yayaw-table/components/forms/types";
import { productForm } from "./product-form";

export const exampleForm: FormConfig = {
  ...productForm,
  fields: productForm.fields.map((field) =>
    field.name === "stock" ? { ...field, required: true } : field
  ),
};
required-stock-form.ts
import type { FormConfig } from "@/components/ui/yayaw-table-vue/types";
import { productForm } from "./product-form";

export const exampleForm: FormConfig = {
  ...productForm,
  fields: productForm.fields.map((field) =>
    field.name === "stock" ? { ...field, required: true } : field
  ),
};