À propos de ce cours
Chapitre 14 — La gestion des formulaires · 14.8 TP 14 : Validation d'un formulaire en JavaScript
• lire toutes les données d'un formulaire réel, avec des types de champs variés ;
• écrire un ensemble de règles de validation et des messages utiles ;
• valider en direct sans gêner l'utilisateur, puis à l'envoi ;
• envoyer les données avec
fetch et gérer succès et erreurs.Présentation
Le Club Informatique de votre établissement ouvre ses inscriptions en ligne. Le formulaire HTML et sa mise en forme sont prêts :
vous écrivez tout le JavaScript dans inscription.js, sans modifier index.html. Les questions vous guident ; aucune solution n'est donnée.
Voici le comportement attendu quand l'utilisateur tente d'envoyer un formulaire incomplet :

Figure 1 — Résumé des erreurs en haut, message sous chaque champ, champs colorés.
Et quand tout est correct et que le serveur a répondu :

Figure 2 — Le formulaire est remplacé par la confirmation.
Cahier des charges
| Champ | Règle |
|---|---|
| Nom, Prénom | obligatoires ; lettres (accents compris), espaces, tirets, apostrophes ; 2 à 40 caractères |
| obligatoire ; format nom@domaine.extension | |
| Téléphone | obligatoire ; 8 chiffres commençant par 2 à 9, éventuellement précédés de +216 ; espaces autorisés à la saisie |
| Date de naissance | obligatoire ; au moins 15 ans aujourd'hui |
| Filière | obligatoire (l'option « — Choisissez — » ne compte pas) |
| Année d'études | un bouton coché |
| Centres d'intérêt | de 1 à 3 cases cochées |
| Mot de passe | 8 caractères minimum, avec au moins une minuscule, une majuscule et un chiffre ; jauge de force de 0 à 4 |
| Confirmation | identique au mot de passe |
| Motivation | 30 caractères minimum (300 maximum) ; compteur « n / 300 » en direct |
| Règlement | case cochée |
Fichiers de départ
⬇ Télécharger les fichiers de départ du TP (tp14-depart.zip)
index.html (à ne pas modifier)
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Inscription — Club Informatique</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<main>
<h1>Inscription au Club Informatique</h1>
<!-- Résumé des erreurs, lu par les lecteurs d'écran -->
<div id="resume" role="alert" hidden></div>
<form id="inscription" action="https://jsonplaceholder.typicode.com/users" method="post" novalidate>
<div class="ligne">
<label>Nom <input name="nom" autocomplete="family-name" aria-describedby="err-nom"><span class="erreur" id="err-nom"></span></label>
<label>Prénom <input name="prenom" autocomplete="given-name" aria-describedby="err-prenom"><span class="erreur" id="err-prenom"></span></label>
</div>
<label>E-mail <input name="email" type="email" autocomplete="email" aria-describedby="err-email"><span class="erreur" id="err-email"></span></label>
<div class="ligne">
<label>Téléphone <input name="tel" inputmode="tel" placeholder="+216 98 123 456" aria-describedby="err-tel"><span class="erreur" id="err-tel"></span></label>
<label>Date de naissance <input name="naissance" type="date" aria-describedby="err-naissance"><span class="erreur" id="err-naissance"></span></label>
</div>
<label>Filière
<select name="filiere" aria-describedby="err-filiere">
<option value="">— Choisissez —</option>
<option value="info">Informatique</option>
<option value="maths">Mathématiques</option>
<option value="sciences">Sciences expérimentales</option>
<option value="eco">Économie et gestion</option>
</select>
<span class="erreur" id="err-filiere"></span>
</label>
<fieldset aria-describedby="err-niveau">
<legend>Année d'études</legend>
<div class="choix">
<label><input type="radio" name="niveau" value="1"> 1re</label>
<label><input type="radio" name="niveau" value="2"> 2e</label>
<label><input type="radio" name="niveau" value="3"> 3e</label>
<label><input type="radio" name="niveau" value="4"> 4e</label>
</div>
<span class="erreur" id="err-niveau"></span>
</fieldset>
<fieldset aria-describedby="err-interets">
<legend>Centres d'intérêt (1 à 3)</legend>
<div class="choix">
<label><input type="checkbox" name="interets" value="web"> Web</label>
<label><input type="checkbox" name="interets" value="jeux"> Jeux vidéo</label>
<label><input type="checkbox" name="interets" value="robotique"> Robotique</label>
<label><input type="checkbox" name="interets" value="ia"> Intelligence artificielle</label>
<label><input type="checkbox" name="interets" value="securite"> Cybersécurité</label>
</div>
<span class="erreur" id="err-interets"></span>
</fieldset>
<div class="ligne">
<label>Mot de passe
<input name="mdp" type="password" autocomplete="new-password" aria-describedby="err-mdp aide-mdp">
<meter id="force" min="0" max="4" low="2" high="3" optimum="4" value="0"></meter>
<small id="aide-mdp">8 caractères minimum, avec majuscule, minuscule et chiffre.</small>
<span class="erreur" id="err-mdp"></span>
</label>
<label>Confirmation <input name="confirmation" type="password" autocomplete="new-password" aria-describedby="err-confirmation"><span class="erreur" id="err-confirmation"></span></label>
</div>
<label>Pourquoi voulez-vous rejoindre le club ?
<textarea name="motivation" rows="4" maxlength="300" aria-describedby="err-motivation compteur"></textarea>
<small id="compteur">0 / 300</small>
<span class="erreur" id="err-motivation"></span>
</label>
<label class="case"><input type="checkbox" name="conditions" aria-describedby="err-conditions"> J'accepte le règlement du club</label>
<span class="erreur" id="err-conditions"></span>
<button>Envoyer mon inscription</button>
</form>
<section id="succes" hidden></section>
</main>
<script src="inscription.js"></script>
</body>
</html>
style.css (à ne pas modifier)
/* L'attribut hidden doit toujours gagner, même face à display: grid */
[hidden] { display: none !important; }
body { font-family: system-ui, sans-serif; background: #eef2f7; color: #263238; margin: 0; }
main { max-width: 720px; margin: 24px auto; padding: 0 16px; }
h1 { font-size: 1.5rem; }
form, #succes { display: grid; gap: 14px; background: #fff; padding: 22px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,.06); }
.ligne { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; align-items: start; }
@media (max-width: 560px) { .ligne { grid-template-columns: 1fr; } }
label { display: grid; gap: 4px; font-weight: 600; font-size: 14px; }
label.case { display: flex; align-items: center; gap: 8px; }
input, select, textarea, button { font: inherit; padding: 8px 10px; border: 1px solid #b0bec5; border-radius: 6px; }
fieldset { border: 1px solid #cfd8dc; border-radius: 8px; }
legend { font-weight: 600; font-size: 14px; }
.choix { display: flex; flex-wrap: wrap; gap: 14px; }
.choix label { display: flex; align-items: center; gap: 6px; font-weight: normal; }
small { color: #607d8b; font-weight: normal; }
meter { width: 100%; height: 10px; }
.erreur { color: #c62828; font-size: 13px; font-weight: normal; min-height: 1em; }
[aria-invalid="true"] { border-color: #c62828; background: #ffebee; }
input[aria-invalid="false"], select[aria-invalid="false"], textarea[aria-invalid="false"] { border-color: #2e7d32; }
button { background: #1565c0; color: #fff; border: none; padding: 12px; font-weight: 700; cursor: pointer; }
button:disabled { background: #90a4ae; cursor: wait; }
#resume { background: #ffebee; border: 1px solid #ef9a9a; border-radius: 8px; padding: 12px 16px; margin-bottom: 14px; }
#resume a { color: #b71c1c; }
#succes { border-left: 6px solid #2e7d32; }
inscription.js (à compléter)
// =====================================================================
// TP 14 — Validation du formulaire d'inscription : fichier de départ
// Ne modifiez pas index.html : tout se fait ici.
// =====================================================================
const form = document.querySelector("#inscription");
// PARTIE 2 — Lire les valeurs du formulaire
// PARTIE 3 — Les règles de validation (une fonction par champ)
// PARTIE 4 — Afficher les erreurs à côté des champs
// PARTIE 5 — Validation en direct, force du mot de passe, compteur
// PARTIE 6 — À l'envoi : résumé des erreurs ou envoi avec fetch
Observez la première règle de
style.css: [hidden] { display: none !important; }. Sans elle, #succes { display: grid }
l'emporterait sur l'attribut hidden et le message de succès serait visible dès le départ. Nous avons rencontré ce bug en testant le TP :
c'est un piège classique quand on cache des éléments avec hidden. 🛠 TP · Partie 1 — Lire le formulaire
Mission : Étudiez index.html avant d'écrire du code.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Listez les 12 champs à valider avec, pour chacun, le type de contrôle et la propriété à lire (leçon 14.3).
- Pourquoi le formulaire porte-t-il l'attribut
novalidate? Que se passerait-il sans lui ? - À quoi servent les attributs
aria-describedbyet les éléments<span class="erreur">? Comment allez-vous retrouver le bonspanà partir du nom d'un champ ? - Quel est le rôle de
<div id="resume" role="alert">? Cherchez ce que faitrole="alert".
Je vérifie / je rends :
- ☐ Le tableau des champs.
🛠 TP · Partie 2 — Lire les valeurs
Mission : Écrivez une fonction lireDonnees() qui renvoie un objet avec toutes les valeurs, propres et au bon type.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Utilisez-vous
FormDataou la lecture champ par champ ? Justifiez. - Quelles valeurs faut-il nettoyer avec
trim()? Comment retirer les espaces du téléphone avant de le valider ? - Comment obtenir la liste des centres d'intérêt cochés ? Et un booléen pour le règlement ?
- Testez votre fonction dans la console (
lireDonnees()) avec un formulaire vide puis rempli.
Je vérifie / je rends :
- ☐
lireDonnees()renvoie un objet correct dans les deux cas.
🛠 TP · Partie 3 — Les règles
Mission : Écrivez une règle par champ : une fonction qui reçoit l'objet des données et renvoie un message d'erreur ou "".
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Pourquoi les règles reçoivent-elles toutes les données, et pas seulement la valeur du champ ? Pensez à la confirmation du mot de passe.
- Écrivez une fonction
age(dateTexte). Testez-la avec une date de naissance d'il y a exactement 15 ans aujourd'hui, et la veille de l'anniversaire. Pourquoi soustraire simplement les années ne suffit-il pas ? - Testez votre règle du téléphone avec au moins 6 cas, dont
+216 98 123 456et12345678. - Pour le mot de passe, préférez-vous une seule expression régulière ou plusieurs tests simples ? Lequel permet un message d'erreur plus précis ?
- Les messages indiquent-ils à l'utilisateur comment corriger ? Relisez-les tous.
Je vérifie / je rends :
- ☐ Un fichier de tests (ou des appels dans la console) qui montre chaque règle sur un cas valide et un cas invalide.
🛠 TP · Partie 4 — Afficher les erreurs
Mission : Écrivez afficher(nom, message) et verifier(nom).
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Où écrivez-vous le message ? Sur quel élément placez-vous
aria-invalidpour un champ simple ? Et pour un groupe de boutons radio ou de cases (indice :closest("fieldset")) ? - Pour les boutons radio et les cases,
form.elements[nom]ne renvoie pas un seul élément. Que renvoie-t-il ? Comment obtenir le premier élément pour y placer le focus ? - Vérifiez que le style vert ou rouge apparaît bien grâce à
aria-invalid, sans ajouter de classe CSS.
Je vérifie / je rends :
- ☐ Un appel à
verifier("email")dans la console affiche ou efface correctement le message.
🛠 TP · Partie 5 — La validation en direct
Mission : Ajoutez la validation pendant la saisie, la jauge de force du mot de passe et le compteur de la motivation.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Appliquez la règle de la leçon 14.5 : vérification à la sortie du champ, puis en direct uniquement pour un champ déjà vérifié. Comment mémorisez-vous les champs déjà « touchés » ?
- Utilisez la délégation d'événements : un seul écouteur
focusoutet un seul écouteurinputsur le formulaire. Comment savez-vous quel champ est concerné ? - Les cases et les boutons radio ne déclenchent pas
focusoutde façon utile : quand les vérifiez-vous ? - La jauge
<meter>doit valoir 0 à 4 (un point par critère). Quel attribut ou quelle propriété modifiez-vous ? - Si l'utilisateur modifie le mot de passe après avoir rempli la confirmation, que doit-il se passer ?
Je vérifie / je rends :
- ☐ Démonstration : erreur affichée en quittant un champ, effacée pendant la correction, jauge et compteur en direct.
🛠 TP · Partie 6 — L'envoi
Mission : À l'envoi : tout vérifier ; s'il y a des erreurs, afficher le résumé et placer le focus ; sinon, envoyer avec fetch et afficher la confirmation.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Construisez le résumé : un lien par erreur, qui place le focus sur le champ concerné au clic. Pourquoi faut-il
preventDefault()sur ces liens ? - Quelles données envoyez-vous ? Faut-il envoyer la confirmation du mot de passe ? (indice : la décomposition
const { a, ...reste } = objet) - Envoyez en JSON vers l'adresse de l'attribut
action. Comment la lisez-vous dans votre code ? - Pendant l'envoi, que doit-il arriver au bouton ? Et après, en cas de succès ou d'échec ?
- En cas de succès, masquez le formulaire et affichez la confirmation avec le prénom, le numéro renvoyé par le serveur, la filière (son libellé, pas sa valeur), l'année et les centres d'intérêt. Pourquoi placer ensuite le focus sur le titre de la confirmation ?
- Testez l'échec en coupant le réseau (F12 › Réseau › Hors ligne). Les saisies sont-elles conservées ?
Je vérifie / je rends :
- ☐ Les trois scénarios de la partie suivante passent.
🛠 TP · Partie 7 — Scénarios de recette
Mission : Avant de rendre votre travail, déroulez ces scénarios et notez le résultat de chacun.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Envoi à vide: le résumé annonce-t-il 12 champs ? Le focus est-il sur « Nom » ? Le lien « Filière » du résumé amène-t-il au bon champ ?
- Saisies invalides: nom « B », e-mail « amina@ecole », téléphone « 12345678 », naissance en 2015, 4 centres d'intérêt, mot de passe « abc », confirmation « abd », motivation « Court ». Chaque champ affiche-t-il le bon message ? Le compteur indique-t-il « 5 / 300 » ?
- Correction en direct: en tapant « Ben Salah », l'erreur du nom disparaît-elle ? La jauge vaut-elle 1, 2, 3 puis 4 pour « abc », « abcdefgh », « Abcdefgh », « Abcdefg1 » ?
- Envoi valide: le serveur reçoit-il le téléphone sans espaces et les centres d'intérêt sous forme de tableau ? La confirmation s'affiche-t-elle à la place du formulaire ?
- Clavier seul: pouvez-vous remplir et envoyer tout le formulaire sans souris ?
Je vérifie / je rends :
- ☐ La fiche de recette complétée (scénario, résultat attendu, résultat obtenu).
🛠 TP · Partie 8 — Bonus
Mission : Allez plus loin.
Questions pour avancer (répondez dans votre compte rendu, puis appliquez dans votre projet) :
- Ajoutez un bouton « œil » qui affiche ou masque le mot de passe.
- Sauvegardez le brouillon dans le
localStorageà chaque saisie (sauf les mots de passe !) et restaurez-le au chargement. Pourquoi exclure les mots de passe ? - Ajoutez un champ photo (
type="file", image de 2 Mo maximum) avec un aperçu. Quel format d'envoi devient alors obligatoire ? - Vérifiez en direct si l'e-mail est déjà inscrit, en interrogeant un serveur pendant la frappe. Comment éviter d'envoyer une requête à chaque lettre ? (cherchez « debounce »)
Je vérifie / je rends :
- ☐ Les bonus réalisés, décrits dans le compte rendu.
Grille d'évaluation indicative
| Critère | Points |
|---|---|
| Lecture des données (types corrects, nettoyage) | 2 |
| Règles conformes au cahier des charges, messages utiles | 5 |
| Affichage des erreurs accessible (aria-invalid, aria-describedby, focus) | 3 |
| Validation en direct, jauge, compteur | 3 |
| Résumé, envoi avec fetch, état du bouton, succès et échec | 4 |
| Fiche de recette et compte rendu | 3 |
| Total (bonus : jusqu'à +3) | 20 |
← 14.7 Envoyer un formulaire au serveur avec fetch