# 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.