Aller au contenu

Les bases du DOM

Quand un navigateur charge une page HTML, il n’en conserve pas le texte tel quel. Il le lit et en construit une représentation en mémoire : une structure d’objets qu’un script JavaScript chargé sur la page peut lire et modifier. Cette structure est le DOM.

Une fois la page chargée, on ne manipule plus le HTML mais le DOM. Cette distinction explique qu’une page puisse changer sans être rechargée : modifier le DOM met aussitôt à jour ce que voit l’utilisateur.

Le terme « arbre » désigne une structure précise : chaque élément (HTMLElement) a un parent et peut avoir des enfants, et l’ensemble descend d’une racine unique (<html>), comme une arborescence de fichiers descend d’un dossier racine.

L’exemple ci-dessous montre une carte affichant une boîte à livres. À gauche, ce que voit l’utilisateur ; à droite, l’arbre DOM correspondant. Survoler un élément d’un côté illumine son jumeau de l’autre ; cliquer sur un nœud affiche ses détails.

Rendu

Boîte du parc Laurier

12 livres · à 450 m

  • roman
  • essai
  • BD
Voir la boîte
Arbre DOM
<article class="carte">
<h2 class="carte__titre">Boîte du parc Laurier</h2>
<p class="carte__desc">12 livres · à 450 m</p>
<ul class="carte__tags">
<li>roman</li>
<li>essai</li>
<li>BD</li>
</ul>
<a href="/boites/14" class="carte__lien">Voir la boîte</a>
</article>
Survolez un élément pour le repérer des deux côtés. Cliquez pour voir ses détails.

Cet inspecteur reproduit ce que fait l’onglet Elements (ou Inspecteur) des outils de développement du navigateur. Chaque chose visible à l’écran correspond à un nœud de l’arbre, et inversement. Savoir circuler entre ces deux vues est une compétence de base du développement web.

  • Nœud (node) : tout élément de l’arbre, une balise, mais aussi le texte qu’elle contient ou un commentaire.
  • Élément (element) : un nœud qui est une balise HTML (<p>, <div>, <a>).
  • Parent : le nœud qui en contient directement un autre.
  • Enfant (child) : un nœud contenu directement dans un autre.
  • Frères (siblings) : des nœuds qui partagent le même parent.
  • Racine (root) : le sommet de l’arbre, l’élément <html>.
VérificationDans la carte ci-dessus, quel est le parent des trois <li> ? Et leurs frères ?

Le parent des trois <li> est le <ul> (la liste des tags). Les trois <li> sont frères entre eux, car ils partagent le même parent. Le <ul> est l’enfant de <article> et le frère de <h2>, <p> et <a>.


Pour modifier un élément, il faut d’abord l’atteindre depuis le JavaScript. Le DOM fournit des méthodes de sélection. Les deux méthodes modernes, employées presque toujours, prennent un sélecteur CSS :

// Le premier élément qui correspond au sélecteur CSS
const titre = document.querySelector(".carte__titre");
// Tous les éléments qui correspondent (une NodeList)
const tags = document.querySelectorAll(".carte__tags li");

Des méthodes plus anciennes restent répandues dans le code existant :

document.getElementById("menu"); // par id (sans le #)
document.getElementsByClassName("carte"); // par classe (collection live)
document.getElementsByTagName("li"); // par balise

Dans du code récent, on préfère querySelector et querySelectorAll : une seule syntaxe à retenir, identique à celle de CSS.

Retourne Si rien ne correspond
querySelector(sel) le premier élément (ou null) null
querySelectorAll(sel) tous les éléments (une NodeList) NodeList vide

Une NodeList ressemble à un tableau et s’itère avec for...of ou forEach, sans en être tout à fait un. Pour utiliser map ou filter, on la convertit d’abord avec Array.from(tags) ou [...tags].


Une fois un élément sélectionné, on accède à ses propriétés comme à celles de n’importe quel objet JavaScript.

const titre = document.querySelector(".carte__titre");
titre.textContent = "Nouvelle boîte"; // remplace le texte
titre.textContent; // lit le texte actuel
const lien = document.querySelector(".carte__lien");
lien.getAttribute("href"); // lire un attribut
lien.setAttribute("href", "/boites/7"); // modifier un attribut
lien.href = "/boites/7"; // raccourci pour les attributs courants

La propriété classList est la façon idiomatique de manipuler les classes, donc l’apparence :

const carte = document.querySelector(".carte");
carte.classList.add("active"); // ajouter une classe
carte.classList.remove("active"); // retirer
carte.classList.toggle("active"); // basculer (ajoute si absente, retire si présente)
carte.classList.contains("active"); // tester (true / false)

Modifier les classes plutôt que les styles directement garde la logique d’apparence dans le CSS. JavaScript décide quand une classe s’applique, CSS décide à quoi elle ressemble.


On peut fabriquer de nouveaux nœuds et les greffer dans l’arbre :

// 1. Créer l'élément
const nouveauTag = document.createElement("li");
nouveauTag.textContent = "poésie";
// 2. L'insérer dans le DOM
const liste = document.querySelector(".carte__tags");
liste.appendChild(nouveauTag); // ajoute à la fin des enfants

Tant qu’un élément créé n’est pas inséré dans l’arbre, il existe en mémoire mais n’apparaît nulle part à l’écran : il reste détaché. C’est l’insertion (appendChild, prepend, before, after) qui le rend visible.

Pour retirer un élément :

nouveauTag.remove(); // se retire lui-même de l'arbre
VérificationOn a fait document.createElement('p') et défini son textContent, mais rien n'apparaît. Pourquoi ?

Parce que l’élément a été créé en mémoire mais jamais inséré dans l’arbre du document. Un élément créé est détaché : il faut l’attacher à un parent déjà présent dans le DOM avec appendChild, prepend, before ou after pour qu’il devienne visible. Créer n’est pas insérer.


Le DOM ne sert pas qu’à modifier la page : il permet aussi de réagir à ce que fait l’utilisateur, qu’il s’agisse d’un clic, d’une frappe au clavier, d’un survol ou de la soumission d’un formulaire. On attache pour cela un écouteur d’événement (event listener) à un élément.

const bouton = document.querySelector(".carte__lien");
bouton.addEventListener("click", (evenement) => {
evenement.preventDefault(); // empêche le comportement par défaut
console.log("La boîte a été cliquée");
});

addEventListener prend deux arguments : le type d’événement ("click", "input", "submit", "mouseover"…) et une fonction exécutée quand il survient. Cette fonction reçoit un objet evenement décrivant ce qui s’est produit.

Les pièces précédentes se combinent ici. Une page interactive forme une boucle : l’utilisateur agit, un événement se déclenche, JavaScript modifie le DOM, et l’affichage change.

// Exemple complet : un compteur de clics
const bouton = document.querySelector("#compteur");
let clics = 0;
bouton.addEventListener("click", () => {
clics++;
bouton.textContent = `Cliqué ${clics} fois`;
});
VérificationÀ quoi sert evenement.preventDefault() dans le clic sur un lien <a> ?

Un lien <a href="..."> a un comportement par défaut : naviguer vers l’URL. preventDefault() annule ce comportement, ce qui permet d’exécuter du JavaScript à la place (par exemple afficher du contenu sans changer de page). Sans cet appel, le navigateur suivrait le lien et quitterait la page avant l’exécution du reste du code.

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.

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.

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.