Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
JavaScript

TP 14 : Validation d'un formulaire en JavaScript

Validation d'un formulaire en JavaScript. Dans cette leçon du cours JavaScript, vous apprendrez à : lire toutes les données d'un formulaire réel, avec des types de champs variés ; écrire un ensemble d...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Validation d'un formulaire en JavaScript. Dans cette leçon du cours JavaScript, vous apprendrez à : 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.

Chapitre 14 — La gestion des formulaires · 14.8 TP 14 : Validation d'un formulaire en JavaScript

🎯 Objectifs de la leçon
• 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 :

Formulaire avec résumé et messages d'erreur
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 :

Message de confirmation d'inscription
Figure 2 — Le formulaire est remplacé par la confirmation.

Cahier des charges

ChampRègle
Nom, Prénomobligatoires ; lettres (accents compris), espaces, tirets, apostrophes ; 2 à 40 caractères
E-mailobligatoire ; format nom@domaine.extension
Téléphoneobligatoire ; 8 chiffres commençant par 2 à 9, éventuellement précédés de +216 ; espaces autorisés à la saisie
Date de naissanceobligatoire ; au moins 15 ans aujourd'hui
Filièreobligatoire (l'option « — Choisissez — » ne compte pas)
Année d'étudesun bouton coché
Centres d'intérêtde 1 à 3 cases cochées
Mot de passe8 caractères minimum, avec au moins une minuscule, une majuscule et un chiffre ; jauge de force de 0 à 4
Confirmationidentique au mot de passe
Motivation30 caractères minimum (300 maximum) ; compteur « n / 300 » en direct
Règlementcase 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
⚠️ Attention
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) :

  1. Listez les 12 champs à valider avec, pour chacun, le type de contrôle et la propriété à lire (leçon 14.3).
  2. Pourquoi le formulaire porte-t-il l'attribut novalidate? Que se passerait-il sans lui ?
  3. À quoi servent les attributs aria-describedby et les éléments <span class="erreur">? Comment allez-vous retrouver le bon span à partir du nom d'un champ ?
  4. Quel est le rôle de <div id="resume" role="alert">? Cherchez ce que fait role="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) :

  1. Utilisez-vous FormData ou la lecture champ par champ ? Justifiez.
  2. Quelles valeurs faut-il nettoyer avec trim()? Comment retirer les espaces du téléphone avant de le valider ?
  3. Comment obtenir la liste des centres d'intérêt cochés ? Et un booléen pour le règlement ?
  4. 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) :

  1. 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.
  2. É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 ?
  3. Testez votre règle du téléphone avec au moins 6 cas, dont +216 98 123 456 et 12345678.
  4. 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 ?
  5. 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) :

  1. Où écrivez-vous le message ? Sur quel élément placez-vous aria-invalid pour un champ simple ? Et pour un groupe de boutons radio ou de cases (indice : closest("fieldset")) ?
  2. 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 ?
  3. 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) :

  1. 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 » ?
  2. Utilisez la délégation d'événements : un seul écouteur focusout et un seul écouteur input sur le formulaire. Comment savez-vous quel champ est concerné ?
  3. Les cases et les boutons radio ne déclenchent pas focusout de façon utile : quand les vérifiez-vous ?
  4. La jauge <meter> doit valoir 0 à 4 (un point par critère). Quel attribut ou quelle propriété modifiez-vous ?
  5. 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) :

  1. 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 ?
  2. Quelles données envoyez-vous ? Faut-il envoyer la confirmation du mot de passe ? (indice : la décomposition const { a, ...reste } = objet)
  3. Envoyez en JSON vers l'adresse de l'attribut action. Comment la lisez-vous dans votre code ?
  4. Pendant l'envoi, que doit-il arriver au bouton ? Et après, en cas de succès ou d'échec ?
  5. 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 ?
  6. 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) :

  1. 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 ?
  2. 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 » ?
  3. 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 » ?
  4. 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 ?
  5. 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) :

  1. Ajoutez un bouton « œil » qui affiche ou masque le mot de passe.
  2. Sauvegardez le brouillon dans le localStorage à chaque saisie (sauf les mots de passe !) et restaurez-le au chargement. Pourquoi exclure les mots de passe ?
  3. Ajoutez un champ photo (type="file", image de 2 Mo maximum) avec un aperçu. Quel format d'envoi devient alors obligatoire ?
  4. 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èrePoints
Lecture des données (types corrects, nettoyage)2
Règles conformes au cahier des charges, messages utiles5
Affichage des erreurs accessible (aria-invalid, aria-describedby, focus)3
Validation en direct, jauge, compteur3
Résumé, envoi avec fetch, état du bouton, succès et échec4
Fiche de recette et compte rendu3
Total (bonus : jusqu'à +3)20

← 14.7 Envoyer un formulaire au serveur avec fetch