# Frontend — Patterns : Forms > Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet. --- ## Pattern : Formulaire robuste avec validation et erreurs explicites ### Synthèse - **Objectif** : garantir des formulaires fiables, compréhensibles et maintenables. - **Contexte** : toute interface avec saisie utilisateur et règles métier. - **Quand l'utiliser** : dès qu'un formulaire dépasse un simple champ isolé. - **Quand l'éviter** : formulaires ultra-simples sans validation réelle. ### Analyse - **Avantages** : - UX claire (l'utilisateur sait quoi corriger) - Moins d'erreurs silencieuses - Base saine pour tests et accessibilité - **Limites / vigilance** : - Peut sembler verbeux sans discipline - Risque de duplication si mal factorisé ### Validation - Validé le : 25-01-2026 - Contexte technique : Front-end agnostique, API HTTP ### Implémentation (exemple minimal) ```txt - Validation côté client (format, champs requis) - Validation côté serveur (règles métier) - Mapping explicite des erreurs serveur → champs UI - Aucun submit silencieux ``` ### Checklist - [ ] Messages d'erreur compréhensibles et localisés - [ ] Validation client + serveur cohérente - [ ] Focus automatique sur le champ en erreur - [ ] États loading / disabled gérés - [ ] Tests sur cas valides et invalides --- ## Pattern : Toggle optimiste avec rollback (React Server Action) ### Synthèse - **Objectif** : masquer la latence serveur sur un toggle boolean en mettant à jour l'UI immédiatement, avec rollback en cas d'erreur. - **Contexte** : toggles boolean (visibilité, disponibilité, settings) où la latence doit être masquée. - **Quand l'utiliser** : toggles sans besoin de re-fetcher l'entité entière après mutation. - **Quand l'éviter** : mutations qui retournent des données complexes → préférer le pattern "Server Action retournant l'entité". ### Validation - Validé le : 21-03-2026 - Contexte technique : React / Next.js App Router — app-template-resto ### Implémentation ```tsx const [optimistic, setOptimistic] = useState(initialValue); async function handleToggle() { const prev = optimistic; setOptimistic(!prev); // update immédiat try { await toggleAction(!prev); router.refresh(); // synchronise le Server Component parent } catch { setOptimistic(prev); // rollback si erreur } } ``` --- ## Pattern : Server Action retournant l'entité — élimination de `router.refresh()` sur create/edit ### Synthèse - **Objectif** : mettre à jour l'état local directement avec les données réelles retournées par le serveur, sans round-trip SSR supplémentaire. - **Contexte** : liste d'items managée côté client (`useState`) avec création et modification via Server Actions. - **Quand l'utiliser** : create et edit d'entités dans une liste. Plus performant que toggle optimiste + `router.refresh()`. - **Quand l'éviter** : simples toggles boolean → le pattern optimiste avec rollback suffit. ### Analyse - **Avantages vs toggle optimiste + `router.refresh()` :** - Zéro aller-retour SSR supplémentaire (~500ms–2s économisés sur mobile) - État local garanti cohérent avec la DB (données réelles, pas calculées localement) - Pas de flash de rechargement - **Limites / vigilance** : - `revalidatePath` reste nécessaire pour invalider le cache des pages publiques ### Validation - Validé le : 22-03-2026 - Contexte technique : React / Next.js App Router — app-template-resto story 3.8 ### Implémentation ```typescript // Repository — retourne l'entité complète export async function createItem(tenantId: string, data: Input): Promise { return prisma.item.create({ data: { tenantId, ...data }, select: { ...fullSelect } }); } // Action — retourne la donnée au client export async function createItemAction(formData: FormData): Promise { const actor = await requireOwner(); const item = await createItem(actor.tenantId, input); revalidatePath("/dashboard/..."); // invalider cache pages publiques return item; // ← clé : retourner l'entité } // Client — mise à jour locale sans round-trip SSR const created = await createItemAction(formData); setItems((prev) => [...prev, created]); // pas de router.refresh() ``` **Pour les entités avec relations :** utiliser un helper `findItemById(tenantId, id)` appelé après la mutation pour retourner la forme complète avec les relations résolues. --- ## Pattern : AppInput Outlined Material adapté thème dark ### Synthèse - **Objectif** : homogénéiser tous les inputs de l'app avec un design "outlined Material" adapté à un thème dark custom (label flottant, encoche opaque calée sur la card parente). - **Contexte** : projet Vue/React avec un thème dark où les inputs natifs cassent le design system (couleur d'encoche, débordement Safari iOS, fond input transparent). - **Quand l'utiliser** : design system app-wide où tous les inputs doivent suivre la même grammaire visuelle. - **Quand l'éviter** : design strictement neutre (inputs natifs OS-style) ou framework UI déjà opinioné (Vuetify, Material UI). ### Analyse - **Avantages** : - design cohérent sur tous les formulaires (login, profil, modales) - encoche calée sur la **card parente**, pas sur le bg global → fusion visuelle propre - `appearance: none` + `min-width: 0` + `min-height: 48px` corrigent les inputs date Safari iOS - **Limites / vigilance** : - les pièges (couleur d'encoche, débordement, fond transparent) sont non-évidents et coûtent du temps à chaque itération si non documentés - `inheritAttrs: false` + séparation manuelle class/style obligatoires pour permettre le layout grid externe sans fuite des attrs HTML ### Validation - Validé le : 01-05-2026 - Contexte technique : Vue 3 Composition API — RL799_V2 ### Composant central ```vue ``` ### Pièges documentés 1. **Encoche du mauvais bg** : utiliser `--color-bg-elevated` (canvas global) au lieu de `--color-surface-raised` (card) → patch coloré visible. Toujours caler sur la couleur de la card parente. Si l'input vit dans un contexte différent (modale, header), exposer `--app-input-notch-bg` en custom property pour override. 2. **Bordure traverse le label** : si le label flottant n'a pas de `background` opaque, la bordure passe derrière le texte. L'encoche n'est pas optionnelle. 3. **Fond transparent obligatoire** : si le fond de l'input est différent de la card, l'encoche révèle un patch. Solution : `background: transparent` → fusion totale. 4. **`placeholder=" "` imposé** : le sélecteur `:placeholder-shown` ne marche que si un placeholder existe. Un espace suffit, n'apparaît pas visuellement. 5. **`inheritAttrs: false` + séparation class/style** : sans ça, un parent qui pose `class="--col-2"` voit cette classe ET tous les attrs HTML atterrir sur le wrapper. ### Variantes - **`AppSelect`** : même base, label toujours flottant haut. Chevron SVG `stroke="currentColor"` 1.5px. - **`AppTextarea`** : label toujours flottant haut, pas de slot trailing. --- ## Pattern : Fusion DRY de composants jumeaux par prop discriminante ### Synthèse - **Objectif** : factoriser deux composants partageant la même UI à 80 %+ avec des contextes d'appel différents, sans extraire un 3ᵉ composant `Body` qui multiplie les fichiers et les indirections. - **Contexte** : ex `ConvocationResponseCard` (autonome, charge ses données) + `ConvocationResponseForm` (reçoit la convocation du parent) — même UI, deux modes de consommation. - **Quand l'utiliser** : diff entre les deux composants tient en une dizaine de `computed`/`v-if` discriminés sur **un seul flag de mode**. - **Quand l'éviter** : - cycles de vie ou stores différents (un consomme un store Pinia, l'autre est purement contrôlé) — le `computed` discriminant pollue tout le composant - UI diverge à > 30 % (sections présentes dans l'un, absentes dans l'autre) ### Analyse - **Avantages** : - une seule source de vérité pour markup et styles - tests structurels consolidés sur un seul fichier - évolution UX synchronisée par construction - **Limites / vigilance** : - **anti-pattern à refuser** : extraire un 3ᵉ composant `Body` partagé entre les deux composants originaux. Multiplie les fichiers, ajoute une couche d'indirection (props drilling, events bubbling) sans réduire la complexité réelle ### Validation - Validé le : 01-05-2026 - Contexte technique : Vue 3 — RL799_V2 (530 lignes dupliquées → 310 lignes uniques) ### Implémentation ```vue ``` ### Critère de décision Si le diff entre les deux composants tient en une dizaine de `computed`/`v-if` discriminés sur **un seul flag de mode**, fusionner. Si ça déborde, garder distincts.