Aller au contenu principal

Formulaires contrôlés

Saisir un pseudo et choisir un niveau : React garde la main sur chaque champ.

Objectifs de la séance​

  • comprendre un champ contrôlé (value + onChange) ;
  • gérer l'envoi du formulaire avec onSubmit et preventDefault ;
  • valider la saisie (nettoyer, vérifier la longueur) et afficher une erreur ;
  • convertir une valeur de <select> en un type précis (Niveau) ;
  • rendre le formulaire accessible (label, htmlFor, role="alert").

Petite vérification​

Avant de commencer, vérifiez si vous savez...

  1. lancer la partie et cliquer deux fois très vite sur une réponse : le score doit n'augmenter qu'une fois
  2. dire ce que fait l'attribut disabled={choix !== null} sur les boutons de réponse
  3. répondre à toutes les questions et lire le texte affiché à la fin
📌 Réponse attendue

Notions théoriques​

Un champ contrôlé​

En HTML, un champ <input> garde sa propre valeur. En React, on préfère que l'état du composant soit la source de vérité : le champ affiche la valeur de l'état (value) et prévient React à chaque frappe (onChange). On parle de champ contrôlé.

const [pseudo, setPseudo] = useState("");

<input
value={pseudo}
onChange={(evenement) => setPseudo(evenement.target.value)}
/>

Le cycle à chaque frappe :

  1. l'utilisateur tape une lettre ;
  2. onChange est appelé, evenement.target.value contient le texte du champ ;
  3. setPseudo met à jour l'état ;
  4. React ré-affiche le composant, et le champ affiche la nouvelle valeur.

À tout moment, pseudo contient exactement ce qui est dans le champ : on peut le valider, le transformer, le compter.

value sans onChange

Un champ avec value mais sans onChange est figé : l'utilisateur ne peut plus rien taper. Les deux attributs vont toujours ensemble.

Envoyer le formulaire : onSubmit​

Un formulaire HTML recharge la page à l'envoi. Dans une application React, on veut le comportement inverse : on annule l'envoi par défaut avec evenement.preventDefault(), puis on traite les données avec notre code.

import type { FormEvent } from "react";

function soumettre(evenement: FormEvent<HTMLFormElement>) {
evenement.preventDefault(); // empêche le rechargement de la page
// ... valider puis utiliser pseudo
}

<form onSubmit={soumettre}>
{/* champs */}
<button type="submit">Commencer</button>
</form>

On écoute onSubmit sur le formulaire (et non onClick sur le bouton) : ainsi la touche Entrée envoie aussi le formulaire.

Valider la saisie​

Une saisie utilisateur est une donnée non fiable : espaces inutiles, champ vide, texte trop long. On la nettoie et on la vérifie avant de l'utiliser.

const pseudoPropre = pseudo.trim(); // retire les espaces autour

if (pseudoPropre.length < 3 || pseudoPropre.length > 20) {
setErreur("Le pseudo doit contenir entre 3 et 20 caractères.");
return;
}

L'erreur est elle-même un état (string | null) affiché sous le champ :

{erreur !== null ? <p className="erreur" role="alert">{erreur}</p> : null}
La validation du navigateur ne suffit jamais

Les attributs HTML comme maxLength ou required aident l'utilisateur, mais ils se contournent facilement. Dans une application avec un serveur, les données sont toujours revalidées côté serveur. La validation côté client sert uniquement à guider l'utilisateur.

Le champ <select> et le type Niveau​

evenement.target.value est toujours un texte (string), même pour un nombre. Or notre état attend un Niveau (1 | 2 | 3). On écrit une petite fonction pure qui convertit en vérifiant :

src/logic/niveau.ts
import type { Niveau } from "../types/quiz";

export function versNiveau(valeur: string): Niveau {
const nombre = Number(valeur);
if (nombre === 1 || nombre === 2 || nombre === 3) {
return nombre;
}
return 1;
}

TypeScript comprend que, dans le if, nombre ne peut valoir que 1, 2 ou 3 : le retour est accepté sans conversion forcée (as).

L'accessibilité des formulaires​

  • chaque champ a un <label htmlFor="..."> relié à l'id du champ : un clic sur le label place le curseur dans le champ, et les lecteurs d'écran annoncent son rôle ;
  • le message d'erreur porte role="alert" : les lecteurs d'écran l'annoncent dès son apparition ;
  • type="submit" sur le bouton d'envoi.

Test de mémorisation/compréhension​


Qu'est-ce qu'un champ contrôlé ?


Quelle valeur lit-on dans le texte tapé par l'utilisateur ?


Pourquoi appeler evenement.preventDefault() dans onSubmit ?


Que se passe-t-il avec <input value={pseudo} /> sans onChange ?


Pourquoi écrire une fonction versNiveau pour lire un select ?


Pourquoi la validation côté client ne suffit-elle jamais ?



TP pour réfléchir et résoudre des problèmes​

Votre mission : créer le formulaire de démarrage d'une partie (pseudo + niveau).

Étape 1 — Convertir le niveau en toute sécurité​

Créez src/logic/niveau.ts.


Bonne pratique - Convertir en vérifiant, jamais en forçant

valeur as Niveau ferait taire TypeScript sans rien vérifier. La fonction versNiveau vérifie vraiment et renvoie une valeur de secours (1) si la donnée est inattendue.

Étape 2 — Les états du formulaire​

Créez src/components/FormulaireDemarrage.tsx. Déclarez le pseudo (texte vide au départ), le niveau (Niveau, 1 au départ) et l'erreur (string ou null).


Bonne pratique - Un état par information

Pseudo, niveau et erreur évoluent indépendamment : trois états simples valent mieux qu'un gros objet difficile à mettre à jour.

Étape 3 — Valider et envoyer​

Écrivez la fonction soumettre : elle annule l'envoi par défaut, nettoie le pseudo, le valide, puis prévient le parent avec onDemarrer.


Bonne pratique - Effacer l'erreur quand tout va bien

setErreur(null) avant d'envoyer évite qu'un ancien message d'erreur reste affiché alors que la saisie est devenue correcte.

Étape 4 — Le champ pseudo contrôlé​

Écrivez le champ de saisie avec son label.


Bonne pratique - maxLength en plus de la validation

maxLength évite la frappe de textes trop longs, mais la vérification dans soumettre reste indispensable : un attribut HTML ne protège de rien.

Étape 5 — Le choix du niveau et le message d'erreur​

Écrivez le champ select qui met à jour le niveau grâce à versNiveau.


Bonne pratique - Tester les cas limites

Essayez : un pseudo vide, un pseudo de deux lettres, un pseudo fait uniquement d'espaces (" "), puis un pseudo correct. Seul le dernier doit afficher Départ : dans la console du navigateur (touche F12).

📌 Une solution

Ce qu'il faut retenir​

NotionRésumé
Champ contrôlévalue={etat} + onChange qui met à jour l'état : l'état est la source de vérité.
onSubmitSur le <form>, avec evenement.preventDefault() pour éviter le rechargement.
ValidationNettoyer (trim), vérifier, afficher une erreur stockée dans un état.
selectLa valeur lue est un texte : la convertir avec une fonction qui vérifie.
Côté client / serveurLa validation du navigateur guide l'utilisateur ; le serveur revalide toujours.
Accessibilitélabel + htmlFor, role="alert" pour l'erreur.

Aperçu de la prochaine séance​

Le formulaire est prêt, mais il ne fait que parler à la console. Dans la prochaine séance, vous apprendrez à remonter l'état : le formulaire prévient App, qui décide quel écran afficher (accueil, partie, résultat).