Retour au cours

frontend / javascript

DOM : sélection et manipulation

Leçon 81 exercice

Explication

Ce que vous allez apprendre

  • Sélectionner un ou plusieurs éléments avec querySelector et querySelectorAll
  • Distinguer une NodeList statique d'une HTMLCollection "live"
  • Choisir entre textContent et innerHTML selon l'origine du contenu à afficher
  • Expliquer pourquoi insérer du HTML venant d'un utilisateur avec innerHTML est dangereux
  • Regrouper des modifications du DOM avec un DocumentFragment pour limiter les reflows

Dans quel contexte ?

Un développeur ajoute une fonctionnalité de commentaires sur un article de blog. Son code affiche le texte saisi par le visiteur avec zoneCommentaire.innerHTML = commentaireUtilisateur. Ça fonctionne en démo, jusqu'à ce qu'un testeur tape <img src=x onerror="alert('piraté')"> dans le champ commentaire : le script s'exécute dans le navigateur de tous les visiteurs qui lisent ce commentaire. Remplacer innerHTML par textContent aurait suffi à neutraliser l'attaque, puisque le texte n'aurait alors jamais été interprété comme du HTML.

1. Le pont entre le code et la page affichée

Jusqu'ici, tout le cours portait sur le langage JavaScript "pur", indépendant de tout environnement. Cette leçon change de terrain : elle aborde le DOM (Document Object Model), la représentation en mémoire de la page HTML que le navigateur construit, et que JavaScript peut lire et modifier en temps réel.

2. D'abord, sélectionner un élément

Sélectionner un élément est la première étape de toute interaction. querySelector et querySelectorAll, basés sur la syntaxe des sélecteurs CSS, ont largement remplacé les anciennes méthodes comme getElementsByClassName.

3. Une nuance utile entre les deux résultats

Ces méthodes retournent des collections légèrement différentes : une NodeList (via querySelectorAll) supporte forEach directement, alors qu'une HTMLCollection "live" (méthodes historiques) se met à jour automatiquement si le DOM change — un comportement parfois surprenant si on ne s'y attend pas.

MéthodeType retournéSe met à jour si le DOM change ?
querySelectorÉlément unique (ou null)Non applicable
querySelectorAllNodeList statiqueNon
getElementsByClassNameHTMLCollection liveOui
getElementsByTagNameHTMLCollection liveOui

4. Le point de vigilance le plus important

Une fois l'élément sélectionné, il faut souvent modifier son contenu. Ici, une distinction est essentielle : textContent traite toujours son contenu comme du texte brut, jamais interprété — c'est la méthode sûre. innerHTML, lui, interprète le contenu comme du HTML, ce qui devient dangereux dès que ce contenu provient d'un utilisateur (une leçon entière, plus loin dans le cours, y sera consacrée).

Piège fréquent

Ne jamais faire element.innerHTML = donneeVenantDUnUtilisateur. Si le contenu doit rester du texte, utilisez textContent. Si vous devez réellement insérer du HTML de confiance (jamais fourni par un utilisateur non filtré), envisagez textContent combiné à la création d'éléments via createElement, ou une bibliothèque de sanitization dédiée.

5. Un dernier réflexe de performance

Modifier le DOM a un coût : chaque insertion peut déclencher un recalcul de la mise en page ("reflow"). Regrouper les modifications via un DocumentFragment, ou via une seule affectation plutôt que des centaines d'insertions unitaires, évite de payer ce coût à chaque étape.

Astuce

Pour insérer 100 éléments dans une liste, construisez-les tous dans un DocumentFragment en mémoire, puis faites un seul liste.appendChild(fragment) : un seul reflow au lieu de cent.

Commandes & code

DOM : sélection et manipulation

Interagir avec la page HTML depuis JavaScript.

js
// --- Selection d'elements ---
const parId = document.getElementById("mon-id");
const parSelecteur = document.querySelector(".ma-classe");         // premier match
const tousLesMatch = document.querySelectorAll("ul.liste li");      // NodeList (tous les matchs)
const parClasse = document.getElementsByClassName("item");            // HTMLCollection LIVE
const parTag = document.getElementsByTagName("li");

// NodeList vs HTMLCollection : NodeList est "statique" (querySelectorAll), HTMLCollection est LIVE
// (elle se met a jour automatiquement si le DOM change)
tousLesMatch.forEach((el) => console.log(el.textContent));    // NodeList supporte forEach
// parClasse.forEach(...)   // ERREUR : HTMLCollection n'a pas forEach nativement

// --- Creation et insertion d'elements ---
const nouvelElement = document.createElement("div");
nouvelElement.textContent = "Contenu genere";
nouvelElement.classList.add("carte", "carte--active");
nouvelElement.setAttribute("data-id", "123");
nouvelElement.dataset.role = "widget";           // acces aux data-* via .dataset

const conteneur = document.querySelector("#conteneur");
conteneur.appendChild(nouvelElement);
conteneur.append(nouvelElement, "texte brut");     // append accepte plusieurs args et du texte
conteneur.prepend(nouvelElement);                    // insere en premier
conteneur.insertBefore(nouvelElement, conteneur.firstChild);

// insertAdjacentHTML : insertion performante sans parser/re-render tout le sous-arbre
conteneur.insertAdjacentHTML("beforeend", "<p>Ajoute directement</p>");

// --- Modification du contenu ---
const element = document.querySelector("#titre");
element.textContent = "Nouveau texte";      // echappe le HTML : SAFE contre le XSS
element.innerHTML = "<strong>Gras</strong>"; // interprete le HTML : DANGEREUX avec du contenu non fiable

// --- Classes et styles ---
element.classList.add("actif");
element.classList.remove("inactif");
element.classList.toggle("visible");           // bascule
element.classList.contains("actif");             // teste la presence
element.style.color = "red";                        // style inline
element.style.setProperty("--couleur-theme", "blue"); // variable CSS custom

// --- Attributs ---
element.getAttribute("href");
element.setAttribute("aria-label", "Fermer");
element.removeAttribute("disabled");
element.hasAttribute("required");

// --- Navigation dans le DOM ---
console.log(element.parentElement);
console.log(element.children);              // enfants elements (pas les text nodes)
console.log(element.childNodes);              // TOUS les noeuds (incluant texte/commentaires)
console.log(element.nextElementSibling);
console.log(element.closest(".carte"));          // remonte jusqu'au premier ancetre matchant

// --- Suppression ---
element.remove();                            // API moderne, directe
// element.parentNode.removeChild(element);  // API historique, plus verbeuse

// --- DocumentFragment : batching des insertions pour la performance ---
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
    const li = document.createElement("li");
    li.textContent = `Item ${i}`;
    fragment.appendChild(li);          // aucun reflow tant qu'on reste dans le fragment
}
document.querySelector("#liste").appendChild(fragment);   // UN SEUL reflow a la fin

// --- Mauvaise pratique vs bonne pratique pour des insertions en boucle ---
// MAUVAIS : provoque un reflow/repaint a CHAQUE iteration
// for (let i = 0; i < 1000; i++) {
//     document.querySelector("#liste").innerHTML += `<li>${i}</li>`;
// }

// BON : construire une chaine puis une seule affectation, ou utiliser un DocumentFragment
const html = Array.from({ length: 1000 }, (_, i) => `<li>${i}</li>`).join("");
document.querySelector("#liste").innerHTML = html;   // un seul reflow

Résumé

  • textContent échappe le contenu (sûr), innerHTML l'interprète (risque XSS avec des données non fiables).
  • HTMLCollection (getElementsByClassName) est "live" ; NodeList (querySelectorAll) est statique.
  • Un DocumentFragment regroupe des insertions multiples en un seul reflow, gain de performance notable.
  • closest() remonte l'arbre jusqu'au premier ancêtre matchant un sélecteur.

Exercices pratiques

1 disponible
1

Mission : le commentaire piégé du blog

Objectif : Corriger une faille d'injection HTML sur une zone de commentaires, et optimiser une insertion de 500 éléments qui rame.

Contexte

Sur le blog de Technologik, un testeur a saisi <img src=x onerror="alert('pirate')"> dans le champ commentaire, et le message d'alerte s'est affiché chez tous les visiteurs de l'article. Le code fautif :

js
const zoneCommentaire = document.querySelector("#commentaires");
zoneCommentaire.innerHTML += `<p>${commentaireUtilisateur}</p>`;

Ce même bloc de code, appelé une fois par commentaire affiché (jusqu'à 500 sur les articles populaires), rend aussi la page très lente au chargement. Tu dois corriger la faille de sécurité et le problème de performance.

Résoudre l’exercice →