Séance 10 — Les formulaires

Enfin, l’utilisateur peut parler

  • Construire un formulaire HTML correct
  • Récupérer ce que l’utilisateur a tapé
  • Vérifier la saisie avant de s’en servir

1. Le grand changement

Jusqu’ici, toutes les données de vos pages étaient écrites par vous dans le code. Aujourd’hui, elles viennent de l’utilisateur.

C’est un vrai changement de nature : l’utilisateur peut taper n’importe quoi. Rien, des lettres à la place d’un nombre, 4 000 caractères… Votre code doit tenir.

2. Le formulaire minimal

seance10.html
<form id="monForm">
  <label for="prenom">Ton prénom :</label>
  <input type="text" id="prenom" />
  <button type="submit">Envoyer</button>
</form>

<p id="reponse"></p>
Balise Rôle
<form> Le conteneur. Regroupe les champs
<label> L’étiquette du champ
<input> Le champ de saisie (auto-fermant)
<button type="submit"> Le bouton qui valide
Important<label for="…"> doit correspondre à id="…"
<label for="prenom">Ton prénom :</label>
<input type="text" id="prenom" />

Le for du label et l’id de l’input doivent être identiques. Deux bénéfices :

  1. Cliquer sur le texte place le curseur dans le champ (pratique sur mobile)
  2. Les lecteurs d’écran annoncent le bon libellé aux personnes aveugles

Un champ sans label est un champ inutilisable pour une partie des visiteurs.

Les types d’input utiles

<input type="text" />      <!-- texte libre -->
<input type="number" />    <!-- nombre : le clavier mobile s'adapte -->
<input type="email" />     <!-- email -->
<input type="date" />      <!-- sélecteur de date -->
<input type="checkbox" />  <!-- case à cocher -->
<input type="range" min="0" max="100" /> <!-- curseur -->

Et deux champs qui ne sont pas des <input> :

<textarea id="message" rows="4"></textarea>

<select id="ville">
  <option value="paris">Paris</option>
  <option value="lyon">Lyon</option>
</select>
AstuceAméliorer le confort en 2 attributs
<input type="text" id="prenom" placeholder="Camille" required />
  • placeholder : le texte grisé d’exemple dans le champ. Ce n’est pas un remplacement du <label> : il disparaît dès qu’on tape.
  • required : le navigateur refuse d’envoyer si le champ est vide. C’est du confort, pas de la sécurité — on vérifiera quand même en JavaScript.

3. Lire ce que l’utilisateur a tapé

Une seule propriété à connaître : .value

const prenom = document.querySelector("#prenom");
console.log(prenom.value);
ImportanttextContent ou value ?
Pour… On utilise
Un <p>, un <h1>, un <li>… .textContent
Un <input>, <textarea>, <select> .value

Un champ de saisie n’a pas de « contenu de texte » : il a une valeur. Se tromper des deux donne undefined ou une chaîne vide, sans message d’erreur.

Avertissement.value est TOUJOURS du texte

Même avec <input type="number" /> :

const age = document.querySelector("#age").value;
console.log(typeof age); // "string" 😱
console.log(age + 1);    // "191" au lieu de 20

Toujours convertir avant de calculer :

const age = Number(document.querySelector("#age").value);
console.log(age + 1); // 20 ✅

C’est le piège du + de la séance 4, qui revient pour de vrai.

4. L’événement submit

Étape 1 — Le premier formulaire qui marche

app.js
const monForm = document.querySelector("#monForm");
const prenom = document.querySelector("#prenom");
const reponse = document.querySelector("#reponse");

monForm.addEventListener("submit", (event) => {
  event.preventDefault();

  reponse.textContent = `Bonjour ${prenom.value} !`;
});

Tapez un prénom, cliquez sur Envoyer : le message apparaît.

Importantevent.preventDefault() : la ligne sans laquelle rien ne marche

Par défaut, valider un formulaire recharge la page (comportement historique du web : les données partaient vers un serveur).

Résultat sans cette ligne : la page clignote, votre message apparaît puis disparaît aussitôt. Vous croyez que votre code ne marche pas.

event.preventDefault() dit : « n’applique pas le comportement par défaut, je m’en occupe moi-même ».

Écrivez-la systématiquement en première ligne de tout écouteur submit.

Quand un événement se produit, le navigateur crée un objet qui le décrit (quel élément, quand, quelle touche…) et le passe en paramètre à votre fonction.

monForm.addEventListener("submit", (event) => { … });
//                                  └─┬─┘
//                                    └── le navigateur le remplit pour vous

Vous n’avez rien à faire : il suffit de le déclarer pour pouvoir l’utiliser. Beaucoup de gens l’appellent e pour aller plus vite.

AstucePourquoi écouter submit sur le <form> plutôt que click sur le bouton ?

Parce que submit se déclenche aussi quand l’utilisateur appuie sur Entrée dans un champ — ce que tout le monde fait naturellement. Écouter le clic du bouton raterait ce cas.

Exercice 10.1

Faites un formulaire avec deux champs (prénom, ville) qui affiche, à la validation : Camille habite à Palaiseau.

seance10.html
<form id="monForm">
  <label for="prenom">Prénom :</label>
  <input type="text" id="prenom" />

  <label for="ville">Ville :</label>
  <input type="text" id="ville" />

  <button type="submit">Envoyer</button>
</form>

<p id="reponse"></p>
app.js
const monForm = document.querySelector("#monForm");
const prenom = document.querySelector("#prenom");
const ville = document.querySelector("#ville");
const reponse = document.querySelector("#reponse");

monForm.addEventListener("submit", (event) => {
  event.preventDefault();
  reponse.textContent = `${prenom.value} habite à ${ville.value}.`;
});

5. Vérifier la saisie

L’utilisateur peut tout casser. On vérifie toujours.

monForm.addEventListener("submit", (event) => {
  event.preventDefault();

  const valeur = prenom.value.trim();

  if (valeur === "") {
    reponse.textContent = "Merci d'entrer un prénom.";
    reponse.className = "erreur";
    return; // on s'arrête ici
  }

  reponse.textContent = `Bonjour ${valeur} !`;
  reponse.className = "ok";
});
ImportantTrois techniques dans ce petit bloc
  1. .trim() enlève les espaces au début et à la fin. Sans lui, une saisie de trois espaces passerait le test « non vide ».

  2. return pour sortir tôt. Dès qu’un problème est détecté, on affiche le message et on quitte. Le reste du code n’a alors pas à se demander si la saisie est valide : elle l’est forcément.

  3. if (valeur === "") teste la chaîne vide, qui est ce que renvoie un champ non rempli.

Vérifier un nombre

const age = Number(champAge.value);

if (Number.isNaN(age)) {
  message.textContent = "Ce n'est pas un nombre.";
  return;
}

if (age < 0 || age > 130) {
  message.textContent = "Cet âge n'est pas plausible.";
  return;
}

Number.isNaN(x) répond « est-ce que la conversion a échoué ? ».

Exercice 10.2 — Le convertisseur °C → °F

Un champ de saisie, un bouton, et l’affichage du résultat. Formule : F = C × 9 / 5 + 32.

Le programme doit refuser poliment une saisie vide ou non numérique.

temperature.html
<form id="conv">
  <label for="celsius">Température en °C :</label>
  <input type="number" id="celsius" placeholder="20" />
  <button type="submit">Convertir</button>
</form>

<p id="resultat"></p>
temperature.js
const conv = document.querySelector("#conv");
const celsius = document.querySelector("#celsius");
const resultat = document.querySelector("#resultat");

function versFahrenheit(c) {
  return (c * 9) / 5 + 32;
}

conv.addEventListener("submit", (event) => {
  event.preventDefault();

  const saisie = celsius.value.trim();

  if (saisie === "") {
    resultat.textContent = "Merci d'entrer une température.";
    return;
  }

  const c = Number(saisie);

  if (Number.isNaN(c)) {
    resultat.textContent = "Ce n'est pas un nombre valide.";
    return;
  }

  resultat.textContent = `${c} °C = ${versFahrenheit(c).toFixed(1)} °F`;
});

La conversion est isolée dans versFahrenheit : elle est testable et réutilisable. L’écouteur, lui, ne s’occupe que de lire, vérifier et afficher.

6. Ajouter des éléments proprement

À la séance 8, on a affiché des listes avec innerHTML. Avec du texte tapé par l’utilisateur, c’est dangereux (rappel : attaque XSS). Voici la méthode propre.

const li = document.createElement("li"); // 1. créer l'élément
li.textContent = texteDeLUtilisateur;    // 2. remplir en TEXTE
liste.appendChild(li);                   // 3. l'accrocher dans la page
ImportantPourquoi c’est sûr

textContent met le contenu tel quel : si l’utilisateur tape <script>…</script>, ces caractères s’affichent comme du texte à l’écran, sans jamais être exécutés.

Avec innerHTML, ils seraient interprétés comme du vrai code.

Règle définitive : texte venant d’un humain → createElement + textContent.

Étape 2 — La liste de tâches

taches.html
<h1>Mes tâches</h1>

<form id="ajoutForm">
  <label for="tache">Nouvelle tâche :</label>
  <input type="text" id="tache" placeholder="Réviser le CSS" />
  <button type="submit">Ajouter</button>
</form>

<p id="erreur"></p>
<ul id="liste"></ul>
<p id="compteur"></p>

<script src="taches.js"></script>
taches.js
const ajoutForm = document.querySelector("#ajoutForm");
const champ = document.querySelector("#tache");
const liste = document.querySelector("#liste");
const erreur = document.querySelector("#erreur");
const compteur = document.querySelector("#compteur");

const taches = [];

function afficher() {
  liste.innerHTML = ""; // on vide avant de redessiner

  for (const tache of taches) {
    const li = document.createElement("li");
    li.textContent = tache;
    liste.appendChild(li);
  }

  compteur.textContent = `${taches.length} tâche(s)`;
}

ajoutForm.addEventListener("submit", (event) => {
  event.preventDefault();

  const texte = champ.value.trim();

  if (texte === "") {
    erreur.textContent = "La tâche ne peut pas être vide.";
    return;
  }

  erreur.textContent = "";
  taches.push(texte);
  champ.value = ""; // on vide le champ pour la saisie suivante
  afficher();
});

afficher();
AstuceLe schéma que vous réutiliserez toute votre vie
    données (un tableau)
           │
           ▼
    afficher()  ── redessine TOUTE la liste à partir des données
           ▲
           │
    une action modifie les données, puis rappelle afficher()

Les données sont la vérité, l’affichage n’en est qu’un reflet. On ne bricole jamais l’affichage tout seul : on change les données et on redessine.

C’est le principe de tous les grands outils du web (React, Vue, Svelte…). Vous venez de l’appliquer à la main.

7. À toi de jouer

Exercice 10.3 — Trouver les 4 erreurs

<form id="f">
  <input type="number" id="nb" />
  <button>Doubler</button>
</form>
<p id="out"></p>
const f = document.querySelector("#f");
const nb = document.querySelector("#nb");
const out = document.querySelector("#out");

f.addEventListener("click", (event) => {
  out.value = nb.textContent + nb.textContent;
});
  1. addEventListener("click", …) sur un <form> → il faut écouter "submit".
  2. event.preventDefault() manquant → la page se recharge, le résultat disparaît instantanément.
  3. nb.textContent → sur un <input>, on lit .value. Et il faut convertir en nombre, sinon "5" + "5" donne "55" au lieu de 10.
  4. out.value → out est un <p> : on écrit dans .textContent.
f.addEventListener("submit", (event) => {
  event.preventDefault();
  const valeur = Number(nb.value);
  out.textContent = valeur * 2;
});

Bonus : il manque un <label> pour le champ, et le bouton devrait porter type="submit" (c’est le comportement par défaut dans un formulaire, mais l’écrire est plus clair).

Exercice 10.4 — Supprimer une tâche

Reprenez la liste de tâches. Ajoutez à chaque ligne un bouton ✕ qui supprime la tâche correspondante.

NoteIndices
  • Pour retirer un élément d’un tableau : taches.splice(position, 1)
  • Dans la boucle de afficher(), taches.indexOf(tache) donne la position
  • Créez le bouton avec createElement("button") et accrochez-le au <li> avec appendChild
taches.js
function afficher() {
  liste.innerHTML = "";

  for (const tache of taches) {
    const li = document.createElement("li");
    li.textContent = tache;

    const supprimer = document.createElement("button");
    supprimer.textContent = "✕";

    supprimer.addEventListener("click", () => {
      const position = taches.indexOf(tache);
      taches.splice(position, 1);
      afficher(); // on redessine tout
    });

    li.appendChild(supprimer);
    liste.appendChild(li);
  }

  compteur.textContent = `${taches.length} tâche(s)`;
}

Le point important : le bouton de suppression ne touche pas au HTML. Il modifie le tableau, puis appelle afficher(). C’est le schéma « données → affichage » de tout à l’heure.

Notez que afficher() s’appelle elle-même indirectement (via l’écouteur) : c’est normal et sans danger ici, car cela n’arrive qu’au moment d’un clic.

Exercice 10.5 — Le formulaire d’inscription 📝

Faites une page inscription.html avec un formulaire complet :

Champ Type Règle de validation
Prénom texte non vide, au moins 2 caractères
Âge nombre entre 15 et 99
Email email doit contenir un @
Formation liste déroulante 3 choix au minimum
Newsletter case à cocher libre

À la validation :

  • Si une règle n’est pas respectée → afficher le premier message d’erreur, en rouge, et ne rien ajouter
  • Sinon → ajouter la personne à un tableau et réafficher la liste complète des inscrits sous la forme : Camille, 19 ans — L2 Informatique — abonnée à la newsletter
NoteIndices
  • Longueur d’un texte : texte.length
  • Contient un @ : email.includes("@")
  • Case cochée : maCase.checked (un booléen)
  • Valeur sélectionnée : maListe.value
inscription.html
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <title>Inscription</title>
    <link rel="stylesheet" href="inscription.css" />
  </head>
  <body>
    <h1>Inscription</h1>

    <form id="formulaire">
      <p>
        <label for="prenom">Prénom :</label>
        <input type="text" id="prenom" placeholder="Camille" />
      </p>

      <p>
        <label for="age">Âge :</label>
        <input type="number" id="age" placeholder="19" />
      </p>

      <p>
        <label for="email">Email :</label>
        <input type="email" id="email" placeholder="camille@exemple.fr" />
      </p>

      <p>
        <label for="formation">Formation :</label>
        <select id="formation">
          <option value="L1">L1 Sciences</option>
          <option value="L2 Informatique">L2 Informatique</option>
          <option value="L3 Physique">L3 Physique</option>
        </select>
      </p>

      <p>
        <input type="checkbox" id="newsletter" />
        <label for="newsletter">Je veux recevoir la newsletter</label>
      </p>

      <button type="submit">S'inscrire</button>
    </form>

    <p id="erreur" class="erreur"></p>

    <h2>Inscrits</h2>
    <ul id="liste"></ul>

    <script src="inscription.js"></script>
  </body>
</html>
inscription.css
body {
  font-family: system-ui, Arial, sans-serif;
  max-width: 700px;
  margin: 0 auto;
  padding: 24px;
}

label {
  display: inline-block;
  width: 110px;
}

.erreur {
  color: #c0392b;
  font-weight: bold;
}
inscription.js
const formulaire = document.querySelector("#formulaire");
const prenom = document.querySelector("#prenom");
const age = document.querySelector("#age");
const email = document.querySelector("#email");
const formation = document.querySelector("#formation");
const newsletter = document.querySelector("#newsletter");
const erreur = document.querySelector("#erreur");
const liste = document.querySelector("#liste");

const inscrits = [];

function afficher() {
  liste.innerHTML = "";

  for (const personne of inscrits) {
    const li = document.createElement("li");
    li.textContent = personne;
    liste.appendChild(li);
  }
}

formulaire.addEventListener("submit", (event) => {
  event.preventDefault();

  const p = prenom.value.trim();
  const a = Number(age.value);
  const e = email.value.trim();

  if (p.length < 2) {
    erreur.textContent = "Le prénom doit faire au moins 2 caractères.";
    return;
  }

  if (Number.isNaN(a) || a < 15 || a > 99) {
    erreur.textContent = "L'âge doit être un nombre entre 15 et 99.";
    return;
  }

  if (!e.includes("@")) {
    erreur.textContent = "L'email doit contenir un @.";
    return;
  }

  erreur.textContent = "";

  let ligne = `${p}, ${a} ans — ${formation.value}`;
  if (newsletter.checked) {
    ligne = ligne + " — abonné·e à la newsletter";
  }

  inscrits.push(ligne);
  afficher();
  formulaire.reset(); // vide tous les champs d'un coup
});

afficher();

Trois remarques :

  • Les vérifications s’enchaînent avec des return : dès qu’une échoue, on s’arrête. Pas besoin de else.
  • formulaire.reset() remet tous les champs à vide en une ligne.
  • On stocke une phrase dans le tableau, ce qui est un peu pauvre : si on voulait trier par âge, il faudrait garder les informations séparées. C’est exactement le rôle des objets — au programme de la séance 11 !

8. Fiche mémo

Le formulaire

<form id="f">
  <label for="champ">Libellé :</label>
  <input type="text" id="champ" placeholder="exemple" />
  <button type="submit">Envoyer</button>
</form>

L’écouteur (toujours la même forme)

f.addEventListener("submit", (event) => {
  event.preventDefault();   // ← SANS ÇA, RIEN NE MARCHE
  …
});

Lire une saisie

champ.value           // toujours du TEXTE
champ.value.trim()    // sans les espaces autour
Number(champ.value)   // converti en nombre
maCase.checked        // true / false

Vérifier

if (texte === "") { … return; }
if (texte.length < 2) { … return; }
if (Number.isNaN(n)) { … return; }
if (!email.includes("@")) { … return; }

Ajouter un élément (méthode sûre)

const li = document.createElement("li");
li.textContent = texteUtilisateur;
liste.appendChild(li);

Vider

liste.innerHTML = "";  // vider un conteneur
champ.value = "";      // vider un champ
f.reset();             // vider tout le formulaire

9. Avant la prochaine séance