# Frontend — Patterns : Forms

> Extrait de la base de connaissance Lead_tech. Voir `knowledge/frontend/patterns/README.md` pour l'index complet.

---

<a id="pattern-formulaire-robuste"></a>
## 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

---

<a id="pattern-toggle-optimiste-rollback"></a>
## 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
  }
}
```

---

<a id="pattern-server-action-retourne-entite"></a>
## 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<ItemRow> {
  return prisma.item.create({ data: { tenantId, ...data }, select: { ...fullSelect } });
}

// Action — retourne la donnée au client
export async function createItemAction(formData: FormData): Promise<ItemRow> {
  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.

---

<a id="pattern-app-input-outlined-material-dark"></a>
## 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
<script setup lang="ts">
defineOptions({ inheritAttrs: false });

const props = defineProps<{
  modelValue?: string | number;
  label: string;             // requis — pas d'input sans label
  type?: string;
  staticLabel?: boolean;     // label toujours haut (utile pour selects)
}>();

const attrs = useAttrs();
// Séparation manuelle class/style — permet layout grid externe (--col-2)
// sans que les attrs HTML fuitent sur le wrapper
const wrapperClass = computed(() => attrs.class);
const wrapperStyle = computed(() => attrs.style);
const inputAttrs = computed(() => {
  const { class: _c, style: _s, ...rest } = attrs;
  return rest;
});
</script>

<template>
  <label :class="['app-input', wrapperClass]" :style="wrapperStyle">
    <input
      :value="modelValue"
      placeholder=" "
      v-bind="inputAttrs"
      class="app-input__control"
      @input="$emit('update:modelValue', $event.target.value)"
    />
    <span class="app-input__label">{{ label }}</span>
  </label>
</template>

<style scoped>
.app-input__control {
  width: 100%;
  min-width: 0;          /* CRITIQUE Safari iOS — sinon débordement */
  min-height: 48px;      /* homogénéise date/datetime-local */
  padding: 12px;
  background: transparent; /* fusion totale avec la card parente */
  border: 1px solid var(--color-border-base);
  border-radius: 6px;
  appearance: none;        /* CRITIQUE iOS pour datetime-local */
  -webkit-appearance: none;
}

/* Label flottant quand input rempli OU focus */
.app-input__control:focus + .app-input__label,
.app-input__control:not(:placeholder-shown) + .app-input__label {
  top: 0;
  font-size: 0.75rem;
  /* Encoche opaque calée sur la CARD parente, pas le bg global */
  background: var(--app-input-notch-bg, var(--color-surface-raised));
}
</style>
```

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

---

<a id="pattern-fusion-dry-composants-jumeaux"></a>
## 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
<script setup lang="ts">
const props = defineProps<{
  // Mode A : Card autonome (consomme un dataset complet)
  data?: ProchaineTenueData;
  // Mode B : Form simple (reçoit juste l'objet à éditer)
  convocation?: ProchaineTenueConvocation;
}>();

const isCardMode = computed(() => props.data !== undefined);

// Convocation dérivée selon le mode → le reste du composant manipule
// uniquement `convocation.value`, sans se soucier du mode
const convocation = computed(() => {
  if (isCardMode.value) {
    const primary = props.data!.primaryGrade;
    return props.data!.gradeInfos[primary]?.convocation;
  }
  return props.convocation;
});

// Émission typée en union — chaque mode émet son type approprié
const emit = defineEmits<{
  updated: [ProchaineTenueData | ConvocationResponseData];
}>();
</script>
```

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