IFT3225

Les bases du DOM permettent de sélectionner des éléments, de modifier leur contenu et de réagir à des actions simples de l’utilisateur. Ces opérations suffisent pour des pages très simples, mais elles deviennent rapidement insuffisantes lorsqu’une interface contient plusieurs boutons, formulaires, listes dynamiques ou composants réutilisables.

Dans une interface plus complète, il faut comprendre comment les événements circulent dans la page, comment gérer efficacement plusieurs éléments semblables, comment lire et valider les données saisies par l’utilisateur, et comment maintenir une cohérence entre les données en mémoire et ce qui est affiché dans le navigateur.

Cette page introduit ces notions avancées du DOM en les reliant à des problèmes fréquents de conception d’interfaces web.

1. Le cycle d’un événement

Lorsqu’un utilisateur clique sur un élément, le navigateur ne transmet pas simplement l’événement à cet élément isolé. Comme les éléments HTML sont imbriqués dans l’arbre du DOM, l’événement est aussi relié aux éléments parents : une image peut être dans un bouton, lui-même dans une carte, elle-même dans une section.

Pour gérer correctement les interactions, il faut donc comprendre le trajet d’un événement dans l’arbre du document. Ce trajet se déroule en trois phases.

  1. Capture : l’événement descend de la racine jusqu’à l’élément cliqué.
  2. Cible : il atteint l’élément effectivement cliqué.
  3. Bouillonnement : il remonte de la cible vers la racine.

Par défaut, vos écouteurs réagissent à la phase de bouillonnement. Pour écouter en capture, on passe une option à addEventListener.

Deux propriétés à ne jamais confondre : event.target est l’élément réellement cliqué, event.currentTarget est l’élément dont l’écouteur s’exécute en ce moment.

Manipulez la séquence ci-dessous : activez la capture, ou coupez la remontée au parent, et observez l’ordre de déclenchement.

Cliquez sur l'enfant pour lancer la propagation.
  1. en attente d'un clic…
event.targetbutton (enfant)
event.currentTargetaucun

La capture descend, du grand-parent vers la cible ; le bouillonnement remonte, de la cible vers le grand-parent. event.target reste l'élément cliqué ; event.currentTarget est celui qui traite l'événement à cet instant.

// écouter en phase de capture (sinon, par défaut, c'est le bouillonnement)
zone.addEventListener("click", gerer, { capture: true });

enfant.addEventListener("click", (e) => {
  console.log(e.target);        // l'élément cliqué
  console.log(e.currentTarget); // l'élément qui écoute (ici, enfant)
  e.stopPropagation();          // stoppe la suite de la propagation
});

// annuler l'action par défaut (navigation d'un lien, envoi d'un formulaire)
lien.addEventListener("click", (e) => e.preventDefault());

stopPropagation() arrête le voyage de l’événement ; preventDefault() n’arrête pas le voyage, mais annule l’action que le navigateur ferait par défaut. Ce sont deux choses distinctes.

VérificationVous cliquez sur un bouton imbriqué dans une carte. Dans le gestionnaire posé sur la carte, que valent `event.target` et `event.currentTarget` ?

event.target est le bouton (l’élément réellement cliqué). event.currentTarget est la carte (l’élément dont l’écouteur s’exécute). C’est précisément cette distinction qui rend la délégation possible.

2. La délégation d’événements

Comme l’événement remonte par bouillonnement, on peut placer un seul écouteur sur un parent plutôt qu’un par enfant. Le parent inspecte event.target pour savoir d’où vient le clic. On retrouve l’élément utile avec closest().

Supposons qu’une page affiche une liste de livres. Une première approche consisterait à ajouter un écouteur click sur chaque élément <li>. Cette approche fonctionne, mais elle devient vite répétitive. Elle pose aussi un problème lorsque de nouveaux livres sont ajoutés dynamiquement : il faut penser à leur attacher de nouveaux écouteurs.

La délégation d’événements consiste à déplacer l’écouteur vers un élément parent stable, par exemple la liste complète. Le parent reçoit les clics qui remontent par bouillonnement et détermine ensuite quel élément enfant a été visé.

const liste = document.querySelector("#livres");

liste.addEventListener("click", (e) => {
  const item = e.target.closest("li"); // remonte jusqu'au <li> le plus proche
  if (!item) return;                   // clic hors d'un item : on ignore
  console.log("livre choisi :", item.dataset.id);
});
L’avantage décisif : un seul écouteur suffit, et il fonctionne aussi pour les éléments ajoutés plus tard, sans avoir à les ré-abonner un par un.

3. Les formulaires

Un formulaire émet trois événements utiles : submit (à l’envoi), input (à chaque frappe) et change (quand la valeur est validée, souvent à la perte du focus). Par défaut, lorsqu’un formulaire est soumis, le navigateur envoie les données selon les attributs du formulaire et recharge généralement la page. Dans une application web interactive, on souhaite souvent empêcher ce comportement afin de valider les données, afficher des messages ou envoyer la requête soi-même avec JavaScript. C’est pourquoi on intercepte l’événement submit et on appelle preventDefault().

const form = document.querySelector("#inscription");

form.addEventListener("submit", (e) => {
  e.preventDefault();              // empêche le rechargement
  if (!form.checkValidity()) {
    form.reportValidity();         // affiche les messages natifs
    return;
  }
  const data = new FormData(form); // lit tous les champs nommés
  const courriel = data.get("courriel");
  // envoyer data vers le serveur...
});

FormData lit d’un coup tous les champs portant un attribut name. La validation se fait d’abord par les contraintes HTML (required, type="email", pattern, min), que l’on complète au besoin par des vérifications en JavaScript.

4. La liaison de données

Dans une interface interactive, certaines informations existent en mémoire JavaScript : le nom saisi par l’utilisateur, la liste des livres, le nombre de clics, l’élément sélectionné, etc. On appelle souvent ces informations l’état de l’interface.

La vue correspond à ce qui est affiché dans le DOM. Le rendu est l’opération qui consiste à transformer l’état en éléments visibles dans la page.

La liaison de données désigne les mécanismes qui gardent l’état et la vue cohérents.

Lier les données, c’est garder cohérents l’état (les données en mémoire) et la vue (le DOM affiché). Deux régimes existent.

Basculez entre les deux modes : en two-way, taper dans la vue met tout à jour ; en one-way, la vue ne fait que refléter l’état.

Modèle (état) · source
Vue (lecture et écriture)

Les deux champs écrivent dans l'état : modifier l'un met à jour l'autre et l'aperçu.

AperçuBonjour, Lino !
champ.addEventListener("input", e => setValeur(e.target.value)); // vue -> état
fonctionRendu();                                                  // état -> vue
// la source de vérité
let etat = { nom: "Lino" };

// état -> vue : on rend l'état dans le DOM
function rendre() {
  document.querySelector("#apercu").textContent = `Bonjour, ${etat.nom} !`;
}

// toute modification passe par l'état, puis par un rendu (one-way)
function setNom(v) {
  etat.nom = v;
  rendre();
}

// vue -> état : on ferme la boucle pour obtenir le two-way
const champ = document.querySelector("#nom");
champ.addEventListener("input", (e) => setNom(e.target.value));

rendre();
VérificationEn liaison bidirectionnelle, pourquoi garder l'état comme source de vérité plutôt que de lire la valeur dans le DOM ?

Parce que l’état est unique et fiable, alors que la même donnée peut apparaître à plusieurs endroits du DOM. En centralisant la vérité dans l’état et en redessinant à partir de lui, toutes les vues restent cohérentes ; lire un peu partout dans le DOM mène vite à des incohérences.

5. Les templates

Lorsqu’une même structure HTML doit être répétée plusieurs fois, par exemple pour afficher une liste de cartes, il peut être tentant de construire une longue chaîne de caractères HTML. Cette approche est fragile : une erreur de syntaxe est difficile à repérer, et l’insertion de contenu utilisateur avec innerHTML peut introduire des risques de sécurité.

L’élément <template> permet de conserver la structure HTML dans le document, puis de la cloner en JavaScript lorsque nécessaire. Il est analysé mais n’est pas rendu : ses images et scripts ne se chargent pas, tant qu’on ne le clone pas. C’est l’outil idéal pour répéter une structure.

<template id="tpl-livre">
  <li class="livre">
    <span class="titre"></span>
    <span class="auteur"></span>
  </li>
</template>
const tpl = document.querySelector("#tpl-livre");
const liste = document.querySelector("#livres");

for (const livre of livres) {
  const noeud = tpl.content.cloneNode(true); // copie profonde du contenu
  noeud.querySelector(".titre").textContent = livre.titre;
  noeud.querySelector(".auteur").textContent = livre.auteur;
  liste.append(noeud);
}
On remplit le clone avec textContent, jamais en concaténant du HTML : le contenu reste du texte, ce qui évite l’injection. Comparé à innerHTML, le template garde la structure au même endroit et ne force pas le navigateur à ré-analyser une chaîne à chaque insertion.

6. Pour aller plus loin : composants et encapsulation

À mesure qu’une interface grandit, manipuler directement le DOM depuis un seul fichier JavaScript devient difficile à maintenir. On cherche alors à regrouper certaines parties de l’interface dans des unités autonomes : des composants.

Un composant rassemble généralement trois éléments :

Les web components offrent une manière native de créer ce type d’unité dans le navigateur. Ils permettent de définir de nouvelles balises HTML personnalisées, comme <menu-utilisateur> ou <carte-produit>.

Cette idée prolonge naturellement les notions vues dans cette page : les événements permettent au composant de réagir, les templates permettent de produire sa structure, et l’état permet de contrôler son affichage.

7. À retenir

Notion Idée clé
Cycle d’un événement Capture (descend), cible, bouillonnement (remonte) ; target contre currentTarget
stopPropagation / preventDefault Arrêter le voyage / annuler l’action par défaut : deux effets distincts
Délégation Un écouteur sur le parent, event.target.closest(), fonctionne pour les éléments futurs
Formulaires preventDefault sur submit, FormData, validation HTML puis JavaScript
Liaison de données One-way : état vers vue ; two-way : état et vue synchronisés ; l’état reste la source de vérité
Templates <template> inerte, content.cloneNode(true), remplir par textContent
Web components extends HTMLElement, customElements.define, cycle de vie, shadow DOM, <slot>