Valider les données saisies
Objectif : Refuser une dépense dont le montant, la date ou la catégorie ne respecte pas les règles définies.
Un champ contrôlé affiche la valeur conservée dans l’état React, mais cela ne suffit pas à garantir que cette valeur est acceptable. Un montant nul, une date invalide ou une catégorie absente des options doit être refusé avant de poursuivre.
Dans cette leçon, vous ajoutez une validation au formulaire existant sans modifier la liste des dépenses. Les champs contrôlés et leurs gestionnaires onChange sont ceux mis en place dans « Contrôler les champs du formulaire ».
Intercepter la soumission
La validation se déclenche lorsque la personne soumet le formulaire. Vous gérez cet événement avec onSubmit sur la balise form, et appelez preventDefault() pour empêcher le navigateur de recharger la page. Le gestionnaire vérifie ensuite les valeurs contrôlées et conserve les erreurs dans un état local.
Le fichier ci-dessous présente le composant complet pour cet état du projet. Il reprend les champs contrôlés montant, categorie et date, ainsi que les catégories reçues par props. La validation rejette un montant qui n’est pas un nombre strictement positif, une date que JavaScript ne peut pas interpréter et une catégorie qui ne figure pas dans les options reçues.
import { useState, type FormEvent } from "react";
import type { Categorie } from "../types";
interface FormulaireDepenseProps {
categories: Categorie[];
}
interface ValeursFormulaire {
montant: string;
categorie: Categorie | "";
date: string;
}
interface ErreursFormulaire {
montant?: string;
categorie?: string;
date?: string;
}
export default function FormulaireDepense({
categories,
}: FormulaireDepenseProps) {
const [valeurs, setValeurs] = useState<ValeursFormulaire>({
montant: "",
categorie: "",
date: "",
});
const [erreurs, setErreurs] = useState<ErreursFormulaire>({});
const [confirmation, setConfirmation] = useState("");
function validerFormulaire(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setConfirmation("");
const nouvellesErreurs: ErreursFormulaire = {};
const montantNumerique = Number(valeurs.montant);
if (valeurs.montant.trim() === "" || !Number.isFinite(montantNumerique) || montantNumerique <= 0) {
nouvellesErreurs.montant = "Saisissez un montant supérieur à 0.";
}
if (!categories.includes(valeurs.categorie as Categorie)) {
nouvellesErreurs.categorie = "Choisissez une catégorie proposée.";
}
if (valeurs.date === "" || Number.isNaN(Date.parse(valeurs.date))) {
nouvellesErreurs.date = "Saisissez une date valide.";
}
setErreurs(nouvellesErreurs);
if (Object.keys(nouvellesErreurs).length === 0) {
setConfirmation("Les valeurs sont valides.");
}
}
return (
<section className="depenses__panneau" aria-labelledby="titre-saisie">
<h2 className="depenses__titre" id="titre-saisie">
Ajouter une dépense
</h2>
<form className="depenses__formulaire" onSubmit={validerFormulaire} noValidate>
<label className="depenses__champ" htmlFor="montant">
Montant
</label>
<input
className="depenses__champ"
id="montant"
name="montant"
type="number"
min="0.01"
step="0.01"
required
value={valeurs.montant}
onChange={(event) =>
setValeurs({ ...valeurs, montant: event.target.value })
}
aria-invalid={Boolean(erreurs.montant)}
aria-describedby={erreurs.montant ? "erreur-montant" : undefined}
/>
{erreurs.montant && (
<p className="depenses__erreur" id="erreur-montant" role="alert">
{erreurs.montant}
</p>
)}
<label className="depenses__champ" htmlFor="categorie">
Catégorie
</label>
<select
className="depenses__champ"
id="categorie"
name="categorie"
required
value={valeurs.categorie}
onChange={(event) =>
setValeurs({ ...valeurs, categorie: event.target.value as Categorie | "" })
}
aria-invalid={Boolean(erreurs.categorie)}
aria-describedby={erreurs.categorie ? "erreur-categorie" : undefined}
>
<option value="">Choisir une catégorie</option>
{categories.map((categorie) => (
<option key={categorie} value={categorie}>
{categorie}
</option>
))}
</select>
{erreurs.categorie && (
<p className="depenses__erreur" id="erreur-categorie" role="alert">
{erreurs.categorie}
</p>
)}
<label className="depenses__champ" htmlFor="date">
Date
</label>
<input
className="depenses__champ"
id="date"
name="date"
type="date"
required
value={valeurs.date}
onChange={(event) => setValeurs({ ...valeurs, date: event.target.value })}
aria-invalid={Boolean(erreurs.date)}
aria-describedby={erreurs.date ? "erreur-date" : undefined}
/>
{erreurs.date && (
<p className="depenses__erreur" id="erreur-date" role="alert">
{erreurs.date}
</p>
)}
<button type="submit">Vérifier</button>
{confirmation && <p role="status">{confirmation}</p>}
</form>
</section>
);
}Chaque champ invalide reçoit son propre message. L’attribut aria-invalid signale son état, et aria-describedby relie le champ au message correspondant ; role="alert" rend l’erreur perceptible aux technologies d’assistance. Le formulaire utilise noValidate pour laisser le composant afficher ses propres messages plutôt que ceux du navigateur.
Lire le résultat de la validation
Par exemple, si le montant est vide, la catégorie n’est pas choisie et la date est absente, la soumission affiche les trois erreurs près des champs concernés. Si le montant vaut 12.50, la catégorie correspond à une option et la date est valide, les erreurs disparaissent et le message de confirmation apparaît. À ce stade, le composant vérifie les données : il ne crée pas encore de dépense et ne modifie pas la collection.
À retenir
- Traitez la soumission avec onSubmit et empêchez le rechargement de la page avec preventDefault().
- Vérifiez explicitement le montant, la catégorie sélectionnée et la date avant de poursuivre.
- Conservez les erreurs par champ et reliez chaque message au champ concerné.
- Une validation réussie n’ajoute pas encore la dépense : cette mise à jour sera traitée dans la prochaine leçon.