Formation publique · Jour 4 / 6

Événements & interfaces interactives

Sections

  1. 01 addEventListener & l’objet event (50 min)
  2. 02 Phases, default, propagation (45 min)
  3. 03 Event delegation (le pattern pro) (60 min)
  4. 04 Forms : validation & FormData (50 min)
  5. 05 Keyboard & mouse essentials (30 min)
  6. Extra 1 Le bouton qui fuit
  7. Récap Lab libre

1 addEventListener & l’objet event (50 min)

Jusqu’ici le code s’exécutait une fois, au chargement. Maintenant le code réagit à l’utilisateur.

1.1 Pourquoi addEventListener

On attache une fonction à un élément. Quand l’événement se produit, la fonction est appelée.

On n’utilise plus les attributs HTML onclick="…". Trop fragile, trop difficile à retirer, trop mélangé avec le HTML.

1.2 Attacher un écouteur

const btn = document.querySelector("#my-btn");

btn.addEventListener("click", function (event) {
  console.log("cliqué");
});

Premier argument : le type d’événement (chaîne).

Deuxième argument : la fonction à exécuter (le handler).

1.3 L’objet event

Le navigateur passe automatiquement un objet event au handler.

Propriétés utiles dès le premier jour :

  • event.type – le type ("click", "input"…)
  • event.target – l’élément exact qui a déclenché l’événement
  • event.currentTarget – l’élément sur lequel le listener est attaché
  • event.timeStamp – moment de l’événement (ms)
btn.addEventListener("click", function (event) {
  console.log(event.type);
  console.log(event.target);
  console.log(event.currentTarget);
  console.log(event.timeStamp);
});

1.4 target vs currentTarget

Ils sont souvent identiques au début.

Ils divergent dès qu’on a des éléments imbriqués ou de la délégation (section suivante).

document.body.addEventListener("click", function (event) {
  console.log("target:", event.target);
  console.log("currentTarget:", event.currentTarget);
});

Si vous cliquez sur le bouton, event.target est le bouton. event.currentTarget reste document.body, car le listener est attaché au body.

Habitude saine : loguer les deux systématiquement pendant les exercices.

1.5 removeEventListener

Pour retirer un écouteur, il faut passer exactement la même référence de fonction.

function handleClick(event) {
  console.log("cliqué");
}

btn.addEventListener("click", handleClick);
// plus tard…
btn.removeEventListener("click", handleClick);

Une fonction anonyme ne peut pas être retirée proprement. Préférez une fonction nommée quand vous savez que vous devrez la retirer.

À essayer

  • Ouvrez le dossier www/day-4/s1_addEventListener.
  • Travaillez uniquement dans scripts.js.
  • Suivez les commentaires dans l’ordre.
  • Après chaque bloc : sauvegardez + rafraîchissez.
  • Ouvrez les DevTools (F12) → onglet Elements → sélectionnez un élément → regardez le panneau Event Listeners à droite.

Checkpoint
Vous savez attacher un écouteur avec addEventListener, vous inspectez l’objet event (type, target, currentTarget), et vous savez retirer un écouteur avec removeEventListener.

2 Phases, default, propagation (45 min)

Un événement ne se contente pas d’arriver sur un élément. Il voyage.

2.1 Les phases (version légère)

Trois phases :

  1. Capture – de la racine vers la cible
  2. Target – sur l’élément exact
  3. Bubble – de la cible vers la racine

Par défaut, addEventListener écoute en phase de bubble.

Règle pratique : on travaille presque toujours en bubble. La phase de capture existe, on la connaît, on ne s’en sert que rarement.

// phase de bubble (défaut)
el.addEventListener("click", handler);

// phase de capture (3e argument true) – rare
el.addEventListener("click", handler, true);

2.2 preventDefault

Empêche l’action par défaut du navigateur.

Exemples classiques :

  • un lien ne navigue plus
  • un formulaire ne se soumet plus
  • une touche n’insère plus de caractère
link.addEventListener("click", function (event) {
  event.preventDefault();
  console.log("navigation bloquée");
});

preventDefault ne stoppe pas la propagation. L’événement continue de voyager.

2.3 stopPropagation

Empêche l’événement de continuer vers les autres écouteurs (parents ou plus loin).

child.addEventListener("click", function (event) {
  event.stopPropagation();
  console.log("arrêté ici, le parent ne verra rien");
});

Utile quand un écouteur parent ne doit pas réagir à un clic sur un enfant précis.

2.4 Quand les utiliser vraiment

preventDefault → quand le navigateur fait quelque chose que vous ne voulez pas.

stopPropagation → quand un parent écoute et que vous voulez isoler un enfant.

Les deux peuvent se combiner. Ils restent indépendants.

À essayer

  • Ouvrez le dossier www/day-4/s2_phases-preventDefault.
  • Travaillez uniquement dans scripts.js.
  • Suivez les commentaires dans l’ordre.
  • Après chaque bloc : sauvegardez + rafraîchissez.
  • Dans les DevTools : Sources → Event Listener Breakpoints → cochez Mouse → click (et éventuellement Control → submit). Vous verrez le débogueur s’arrêter sur vos handlers.

Checkpoint
Vous savez expliquer les trois phases en une phrase, vous utilisez preventDefault pour bloquer une action par défaut, et vous savez quand stopPropagation est nécessaire.

3 Event delegation (le pattern pro) (60 min)

Attacher un écouteur à chaque bouton fonctionne… jusqu’à ce que la liste change.

3.1 Le piège des écouteurs multiples

Une boucle qui fait addEventListener sur chaque enfant pose trois problèmes :

  • plus d’éléments = plus d’écouteurs (mémoire et clarté)
  • les éléments ajoutés plus tard n’ont pas d’écouteur
  • retirer un élément oblige à gérer aussi son écouteur

C’est le pattern naïve. On l’écrit une fois pour le sentir, puis on l’abandonne.

3.2 Un seul écouteur sur le parent

On attache l’écouteur au conteneur. L’événement bubble jusqu’à lui.

event.target indique l’élément exact qui a été cliqué.

const list = document.querySelector("#task-list");

list.addEventListener("click", function (event) {
  console.log(event.target); // l’élément réellement cliqué
});

3.3 closest – le filet de sécurité

Le clic peut atterrir sur un enfant du bouton (icône, <span>, texte…).

event.target.closest("selector") remonte jusqu’à trouver l’élément qui nous intéresse.

list.addEventListener("click", function (event) {
  const btn = event.target.closest("button.delete");
  if (!btn) return;          // le clic n’était pas sur un bouton delete

  const item = btn.closest("li");
  item.remove();
});

Un seul écouteur. Fonctionne pour les éléments déjà présents et pour ceux ajoutés plus tard.

3.4 Règle pratique

Dès qu’une liste ou un groupe de boutons peut grandir ou changer, utilisez la délégation.

C’est le pattern professionnel par défaut pour les interfaces dynamiques.

À essayer

  • Ouvrez le dossier www/day-4/s3_event-delegation.
  • Travaillez uniquement dans scripts.js.
  • Suivez les commentaires dans l’ordre (d’abord la version naïve, puis la délégation).
  • Après chaque bloc : sauvegardez + rafraîchissez.
  • Dans Elements → sélectionnez le conteneur → regardez le panneau Event Listeners. Avec la délégation vous ne devez voir qu’un seul écouteur.

Checkpoint
Vous savez expliquer pourquoi les écouteurs multiples sont un piège, vous utilisez un seul écouteur sur le parent avec event.target / closest, et vous vérifiez dans Elements qu’il n’y a bien qu’un seul listener.

4 Forms : validation & FormData (50 min)

Un formulaire qui envoie n’importe quoi est inutile. On valide, puis on lit les valeurs proprement.

4.1 Validation native (HTML)

Le navigateur sait déjà vérifier beaucoup de choses :

  • required – champ obligatoire
  • type="email", type="number", type="url"
  • minlength / maxlength
  • min / max (pour les nombres)
  • pattern – expression régulière simple (optionnel)

Ces attributs donnent une première ligne de défense gratuite et accessible.

4.2 Empêcher la soumission

Même avec la validation native, on écoute l’événement submit et on appelle preventDefault() pour garder le contrôle.

form.addEventListener("submit", function (event) {
  event.preventDefault();

  if (!form.checkValidity()) {
    form.reportValidity(); // affiche les messages natifs
    return;
  }

  // ici le formulaire est valide
});

checkValidity() retourne true/false. reportValidity() affiche les bulles d’erreur du navigateur.

4.3 FormData – lire les valeurs proprement

Éviter de faire un querySelector pour chaque champ.

const data = new FormData(form);

console.log(data.get("email"));
console.log(data.get("age"));

// ou tout parcourir
for (const [key, value] of data.entries()) {
  console.log(key, value);
}

FormData lit les champs qui ont un attribut name. Sans name, la valeur est ignorée.

4.4 Controlled vs uncontrolled (version légère)

Uncontrolled : le navigateur garde la valeur. On la lit seulement au moment utile (souvent au submit) via FormData ou .value.

Controlled : on stocke la valeur dans une variable JS et on la met à jour à chaque input. Plus de travail, utile quand l’interface doit réagir en direct.

Pour la majorité des formulaires classiques, l’approche uncontrolled + FormData suffit et reste plus simple.

À essayer

  • Ouvrez le dossier www/day-4/s4_forms-validation.
  • Travaillez uniquement dans scripts.js.
  • Suivez les commentaires dans l’ordre.
  • Après chaque bloc : sauvegardez + rafraîchissez.
  • Dans Sources → Event Listener Breakpoints → cochez Control → submit. Le débogueur s’arrêtera sur votre handler.

Checkpoint
Vous savez utiliser les attributs de validation native, bloquer la soumission avec preventDefault + checkValidity, et lire toutes les valeurs d’un formulaire avec FormData.

Really safe?
Dans la formation liée à la cybersécurité des applications web, nous reparlerons de ceci... la validation coté client est-elle réellement suffisante?

On se doute que non :-)

5 Keyboard & mouse essentials (30 min)

On ne catalogue pas tous les événements. On retient ceux qui servent vraiment.

5.1 Clavier – keydown et keyup

keydown se déclenche quand la touche est enfoncée (répété si on maintient).

keyup se déclenche quand la touche est relâchée.

Propriété utile : event.key (la valeur lisible : "a", "Enter", "Escape", "ArrowUp"…).

document.addEventListener("keydown", function (event) {
  console.log(event.key);
});

Pour bloquer une touche précise (ex. Enter dans un champ) : event.preventDefault().

5.2 Souris – click, mousedown, mouseup

click = mousedown + mouseup sur le même élément. C’est le plus courant.

mousedown / mouseup servent quand on a besoin de réagir dès l’appui ou au relâchement (drag basique, feedback immédiat).

Dans 90 % des interfaces, click suffit.

5.3 Position du pointeur

Quand la position compte :

  • event.clientX / event.clientY – par rapport à la fenêtre
  • event.offsetX / event.offsetY – par rapport à l’élément cible

On les utilise seulement si l’interface en a vraiment besoin (dessin, positionnement, tooltips…).

5.4 Rappel target

event.target reste la propriété la plus utile : quel élément a réellement reçu l’événement.

Combiné avec closest, c’est la base de la délégation vue juste avant.

À essayer

  • Ouvrez le dossier www/day-4/s5_keyboard-mouse.
  • Travaillez uniquement dans scripts.js.
  • Suivez les commentaires dans l’ordre.
  • Après chaque bloc : sauvegardez + rafraîchissez.
  • Gardez la Console ouverte. Testez aussi les Event Listener Breakpoints (Keyboard → keydown, Mouse → click).

Checkpoint
Vous savez écouter keydown/keyup et lire event.key, vous savez quand utiliser click plutôt que mousedown/mouseup, et vous savez récupérer clientX/clientY ou offsetX si la position est nécessaire.

Extra 1 — Le bouton qui fuit

Fin de journée optionnelle.
Objectif : s’amuser un peu avec mousemove et le positionnement absolu.

Un bouton « click me » apparaît dans une des 9 zones prédéfinies de l’écran. Dès que la souris s’approche, il saute ailleurs.

Pas de nouvelle théorie. Juste de la pratique légère avec ce qui a été vu aujourd’hui (événements souris, coordonnées, manipulation du DOM).

À essayer

  • Ouvrez le dossier www/day-4/x1_moving-button.
  • Travaillez dans scripts.js.
  • Suivez les commentaires.
  • Le but est que le bouton soit réellement difficile à attraper.

Copions

  1. btn.style.left = zone.left + "%";
    btn.style.top = zone.top + "%";

    Convertit les pourcentages de la zone en valeurs CSS utilisables.

  2. const index = Math.floor(Math.random() * ZONES.length);

    Produit un index aléatoire valide entre 0 et 8.

  3. document.addEventListener("mousemove", function (event) {
      console.log(event.clientX, event.clientY);
    });

    Fournit les coordonnées de la souris à chaque mouvement.

  4. const rect = btn.getBoundingClientRect();

    Donne la position et les dimensions actuelles du bouton.

  5. const btnCenterX = rect.left + rect.width / 2;
    const btnCenterY = rect.top + rect.height / 2;

    Calcule le centre du bouton. Le coin supérieur gauche ne suffit pas.

  6. const dx = event.clientX - btnCenterX;
    const dy = event.clientY - btnCenterY;

    Mesure l’écart horizontal et vertical entre la souris et le bouton.

  7. const distance = Math.sqrt(dx * dx + dy * dy);

    Calcule la distance réelle entre les deux points.

  8. let next;
    do {
      next = Math.floor(Math.random() * ZONES.length);
    } while (next === currentZone);

    Choisit une zone différente. Un saut immobile serait assez décevant.

  9. canFlee = false;
    setTimeout(function () {
      canFlee = true;
    }, 250);

    Réactive la fuite après 250 ms pour éviter les déplacements en rafale.

  10. btn.addEventListener("click", function () {
      caught = true;
      btn.textContent = "bravo.";
    });

    Marque le bouton comme attrapé. Le handler mousemove peut alors cesser de le déplacer.

Quand vous en avez assez (ou que vous l’avez attrapé une fois), on s’arrête.

Récap — Lab libre

Pour ce lab, découvrez la version complète de Jem Tanks IO, puis travaillez dans le dossier client-student du projet jem-tanks-io.

Le serveur WebSocket est déjà en ligne : lancez uniquement le client avec npm run student:start. Le port 5173 est recommandé. Si vous lancez Vite autrement, utilisez localhost ou 127.0.0.1 sur le port 5173 ou 8000, conformément à DEFAULT_ALLOWED_ORIGINS dans server/src/server.js.