Chapitre 720 minDébutant
Le DOM : querySelector, textContent et innerHTML
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer ce qu'est le DOM et lire l'arbre d'une page ;
- sélectionner des éléments avec
querySelectoretquerySelectorAll; - modifier un texte avec
textContentet dire pourquoiinnerHTMLexpose à la faille XSS ; - créer, insérer et supprimer des éléments ;
- afficher une liste à partir d'un tableau de données.
4 exercices corrigés1 schéma
Le DOM (Document Object Model) est la représentation de la page HTML sous forme d'arbre d'objets, que le navigateur construit en lisant le code et que JavaScript peut lire et modifier. Changer un objet du DOM change immédiatement ce qui est affiché. Ce chapitre explique comment sélectionner un élément, modifier son texte sans ouvrir de faille XSS, changer ses classes et ses attributs, créer et supprimer des éléments, puis afficher une liste entière à partir d'un tableau de données.
Le DOM, un arbre d'objets
Quand le navigateur charge une page, il transforme chaque balise en nœud élément, chaque texte en nœud texte, et les range dans un arbre : html contient head et body, body contient header et main, et ainsi de suite. Le point d'entrée de cet arbre, pour ton script, est l'objet document.
Voici une version réduite de la page de Gestock :
<!doctype html>
<html lang="fr">
<head>
<title>Gestock : le stock de Réparinfo</title>
</head>
<body>
<header>
<h1>Gestock</h1>
<p id="resume"></p>
</header>
<main>
<table>
<tbody id="liste-pieces"></tbody>
</table>
</main>
</body>
</html>
Le DOM n'est pas le fichier HTML : c'est l'état actuel de la page. Le tbody est vide dans le fichier ; une fois que JavaScript y a ajouté des lignes, l'onglet Éléments (Chrome, Edge) ou Inspecteur (Firefox) des outils de développement les montre, alors que « Afficher le code source » montre toujours le fichier d'origine.
Sélectionner des éléments : querySelector et querySelectorAll
document.querySelector(selecteur) renvoie le premier élément qui correspond à un sélecteur CSS, ou null s'il n'y en a aucun. querySelectorAll renvoie tous les éléments correspondants, dans une liste (NodeList), éventuellement vide.
const resume = document.querySelector('#resume'); // par identifiant
const tbody = document.querySelector('#liste-pieces');
const premiereAlerte = document.querySelector('tr.alerte'); // par balise et classe
const champRef = document.querySelector('input[name="ref"]'); // par attribut
const lignesAlerte = document.querySelectorAll('tr.alerte');
lignesAlerte.length; // le nombre de lignes en alerte
lignesAlerte.forEach((tr) => tr.classList.add('clignote'));
const refs = [...lignesAlerte].map((tr) => tr.cells[0].textContent);
Les sélecteurs sont ceux du CSS (cours HTML et CSS) : #id, .classe, balise, [attribut="valeur"], et leurs combinaisons (main table td). Tu peux aussi chercher à l'intérieur d'un élément : formulaire.querySelector('.erreur') ne regarde que dans le formulaire.
| Méthode | Renvoie | Si rien ne correspond |
|---|---|---|
document.querySelector('#resume') | Le premier élément | null |
document.querySelectorAll('tr.alerte') | Une NodeList figée (statique) | Une liste vide |
document.getElementById('resume') | L'élément d'identifiant resume (sans #) | null |
element.closest('tr') | Le plus proche ancêtre (ou lui-même) qui correspond | null |
Une NodeList a une méthode forEach et se parcourt avec for…of, mais n'a ni map ni filter : convertis-la d'abord en tableau avec [...liste] ou Array.from(liste).
Lire et modifier le contenu d'un élément
| Propriété | Ce qu'elle lit ou écrit | Usage |
|---|---|---|
textContent | Le texte brut, balises ignorées à la lecture, jamais interprétées à l'écriture | Afficher une donnée : le choix par défaut |
innerHTML | Le code HTML, interprété à l'écriture | Insérer du balisage que tu as écrit, jamais une donnée |
value | La valeur d'un champ de formulaire (toujours une chaîne) | Lire ou préremplir un input, select, textarea |
checked | L'état d'une case à cocher (booléen) | Lire une case |
innerText | Le texte visible, selon le CSS | Rarement utile : plus lent que textContent |
resume.textContent = '5 références, 2 en alerte';
console.log(resume.textContent); // '5 références, 2 en alerte'
const champNom = document.querySelector('#nom');
champNom.value = 'SSD NVMe 1 To'; // préremplit le champ
textContent ou innerHTML : la faille XSS
La différence entre les deux propriétés n'est pas une question de style : c'est une question de sécurité. Imagine qu'un technicien saisisse ce nom de pièce dans le formulaire : Chargeur <b>neuf</b>.
cellule.textContent = nomSaisi; // affiche : Chargeur <b>neuf</b> (le texte tel quel)
cellule.innerHTML = nomSaisi; // affiche : Chargeur neuf (avec « neuf » en gras)
Avec innerHTML, le navigateur a interprété la saisie comme du code HTML. Une balise de mise en gras ne fait pas de mal. Mais la même porte laisse passer n'importe quelle balise, y compris une balise dont un attribut contient du JavaScript : le navigateur l'exécute alors, dans ta page, avec les droits de l'utilisateur connecté. C'est le cross-site scripting (XSS), l'une des failles les plus répandues du web, classée dans la catégorie Injection du Top 10 de l'OWASP.
Ce qu'un attaquant peut faire avec un XSS : lire ce que la page affiche, lire localStorage (et donc un jeton qui y serait rangé, chapitre 9), envoyer des requêtes au serveur au nom de la victime, remplacer la page par un faux formulaire de connexion. Si le nom piégé est enregistré dans la base, chaque personne qui affiche la liste des pièces est touchée : c'est un XSS stocké.
Les règles qui suppriment le risque :
- Affiche les données avec
textContent, jamais avecinnerHTML. Une donnée, c'est tout ce qui vient de l'utilisateur, de l'URL, delocalStorageou d'une API, même la tienne. - Construis la structure avec
createElement(section suivante), et remplis chaque élément avectextContent. - Si tu dois vraiment afficher du HTML venu d'ailleurs (un texte mis en forme par un éditeur), fais-le passer par une bibliothèque de nettoyage reconnue comme DOMPurify, qui retire tout ce qui peut s'exécuter.
- Côté serveur, une politique de sécurité de contenu (en-tête
Content-Security-Policy) limite les scripts autorisés : c'est une deuxième ligne de défense, pas une excuse pour utiliserinnerHTML.
// À NE PAS FAIRE : la saisie devient du HTML
tbody.innerHTML += `<tr><td>${piece.ref}</td><td>${piece.nom}</td></tr>`;
// À FAIRE : la saisie reste du texte
const tr = document.createElement('tr');
const td = document.createElement('td');
td.textContent = piece.nom;
tr.append(td);
tbody.append(tr);
Les variantes du XSS (réfléchi, stocké, basé sur le DOM), l'en-tête CSP et l'échappement côté serveur sont détaillés dans le chapitre Faille XSS du cours Sécurité applicative.
Modifier les classes, les attributs et le style
Pour changer l'apparence d'un élément, ajoute ou retire une classe et laisse le CSS faire le reste. C'est plus propre que de modifier le style directement, et le designer garde la main sur les couleurs.
const ligne = document.querySelector('tr');
ligne.classList.add('alerte'); // ajoute la classe
ligne.classList.remove('alerte'); // la retire
ligne.classList.toggle('alerte'); // l'ajoute si absente, la retire si présente
ligne.classList.toggle('alerte', quantite <= seuil); // l'ajoute si la condition est vraie, la retire sinon
ligne.classList.contains('alerte'); // true ou false
tr.alerte {
background: #fdecee;
font-weight: 600;
}
Les attributs se lisent et s'écrivent avec getAttribute et setAttribute, ou directement par la propriété du même nom pour les attributs standard :
const bouton = document.querySelector('button');
bouton.setAttribute('aria-label', 'Retirer une unité : SSD NVMe 512 Go');
bouton.disabled = true; // propriété booléenne
lien.href = '/pieces/SSD-512';
image.alt = 'Photo de la pièce';
section.hidden = true; // masque l'élément (comme display: none)
Les attributs data-* servent à attacher tes propres données à un élément. JavaScript les lit dans dataset, avec un nom en camelCase :
<button data-action="retirer" data-ref="SSD-512">−</button>
bouton.dataset.action; // 'retirer'
bouton.dataset.ref; // 'SSD-512'
C'est ainsi que les boutons + et − de Gestock sauront de quelle pièce il s'agit (chapitre 8).
Le style en ligne reste possible (element.style.display = 'none', element.style.backgroundColor = 'red', avec les noms CSS en camelCase), mais garde-le pour les valeurs calculées, comme la largeur d'une barre de progression.
Créer, insérer et supprimer des éléments
const tr = document.createElement('tr'); // crée un élément, encore hors de la page
tr.classList.add('alerte');
const td = document.createElement('td');
td.textContent = 'RAM-16';
tr.append(td); // td devient enfant de tr
tbody.append(tr); // la ligne apparaît dans la page
| Méthode | Effet |
|---|---|
parent.append(a, b, …) | Ajoute à la fin de parent (éléments ou textes) |
parent.prepend(a) | Ajoute au début |
element.before(a) / element.after(a) | Insère juste avant ou après element |
element.remove() | Retire element de la page |
parent.replaceChildren(a, b, …) | Remplace tout le contenu de parent ; sans argument, le vide |
element.cloneNode(true) | Copie un élément et tout son contenu |
append accepte aussi du texte : p.append('Stock : ', strong) ajoute un nœud texte puis un élément, sans aucune interprétation HTML.
Pour une structure plus longue, l'élément <template> du HTML évite d'écrire dix createElement : tu décris la ligne une fois dans la page, puis tu la clones et remplis ses cases avec textContent.
<template id="modele-ligne">
<tr><td class="ref"></td><td class="nom"></td><td class="quantite"></td></tr>
</template>
const modele = document.querySelector('#modele-ligne');
const ligne = modele.content.cloneNode(true); // un fragment contenant la ligne
ligne.querySelector('.ref').textContent = piece.ref;
ligne.querySelector('.nom').textContent = piece.nom;
ligne.querySelector('.quantite').textContent = piece.quantite;
tbody.append(ligne);
Se déplacer dans l'arbre
| Propriété ou méthode | Donne |
|---|---|
element.parentElement | Le parent |
element.children | Les enfants éléments (sans les nœuds texte) |
element.firstElementChild, lastElementChild | Le premier, le dernier enfant |
element.nextElementSibling | L'élément suivant au même niveau |
element.closest('sélecteur') | Le plus proche ancêtre correspondant, lui-même compris |
ligne.cells | Les cellules d'une ligne de tableau (tr) |
closest est la plus utile : depuis un bouton cliqué, bouton.closest('tr') retrouve la ligne qui le contient.
Afficher une liste à partir des données
Le principe qui organise toutes les interfaces modernes, React compris : les données sont la seule source de vérité, et une fonction d'affichage reconstruit la partie de la page qui en dépend. On ne modifie pas le tableau HTML cellule par cellule ; on modifie les données, puis on réaffiche.
Voici les fonctions d'affichage de Gestock, extraites du futur fichier vue.js :
const euros = new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' });
function cellule(texte) {
const td = document.createElement('td');
td.textContent = texte;
return td;
}
function lignePiece(piece) {
const tr = document.createElement('tr');
if (piece.quantite <= piece.seuil) tr.classList.add('alerte');
tr.append(
cellule(piece.ref),
cellule(piece.nom),
cellule(String(piece.quantite)),
cellule(euros.format(piece.prix)),
);
return tr;
}
function afficherPieces(tbody, pieces) {
if (pieces.length === 0) {
const tr = document.createElement('tr');
const td = cellule('Aucune pièce ne correspond à ta recherche.');
td.colSpan = 4;
tr.append(td);
tbody.replaceChildren(tr);
return;
}
tbody.replaceChildren(...pieces.map(lignePiece));
}
afficherPieces(document.querySelector('#liste-pieces'), pieces);
pieces.map(lignePiece) transforme chaque pièce en ligne <tr>, et replaceChildren(...lignes) remplace d'un coup tout le contenu du tbody. Appeler afficherPieces une deuxième fois ne duplique rien : l'ancien contenu est remplacé. Et comme chaque texte passe par textContent, un nom de pièce piégé s'affiche tel quel, sans jamais s'exécuter.
Une limite de cette approche : reconstruire les lignes détruit l'élément qui avait le focus. Si l'utilisateur vient de cliquer sur un bouton d'une ligne, il perd sa position au clavier. Le chapitre 13 montre comment la lui rendre.
Exercices
Exercice 1 sur 4 Sélecteurs Réussi À revoir
Écris l'instruction qui sélectionne :
- le paragraphe d'identifiant
resume; - toutes les lignes du
tbodyd'identifiantliste-pieces; - le champ de formulaire dont l'attribut
namevautprix; - le premier bouton dont l'attribut
data-refvautCHG-65; - la ligne de tableau qui contient un bouton déjà sélectionné dans la variable
bouton.
Voir la réponseMasquer la réponse de l'exercice 1
document.querySelector('#resume'); // 1.
document.querySelectorAll('#liste-pieces tr'); // 2.
document.querySelector('[name="prix"]'); // 3.
document.querySelector('button[data-ref="CHG-65"]'); // 4.
bouton.closest('tr'); // 5.
Tu avais trouvé ?
Exercice 2 sur 4 Repérer la faille Réussi À revoir
Ce code affiche les avis des clients sur le site de Réparinfo. Les avis sont saisis par les visiteurs et enregistrés en base. Quelle faille contient-il ? Quelles conséquences ? Réécris la fonction.
function afficherAvis(liste, avis) {
for (const a of avis) {
liste.innerHTML += `<li><strong>${a.auteur}</strong> : ${a.texte}</li>`;
}
}
Voir la réponseMasquer la réponse de l'exercice 2
Les champs auteur et texte, saisis par des visiteurs, sont insérés avec innerHTML : le navigateur les interprète comme du HTML. Un visiteur malveillant peut enregistrer un avis contenant une balise qui exécute du JavaScript. C'est un XSS stocké : le script s'exécute chez chaque personne qui affiche la page des avis, et peut lire ce qu'elle affiche, agir en son nom ou afficher un faux formulaire.
function afficherAvis(liste, avis) {
const items = avis.map((a) => {
const li = document.createElement('li');
const strong = document.createElement('strong');
strong.textContent = a.auteur;
li.append(strong, ` : ${a.texte}`);
return li;
});
liste.replaceChildren(...items);
}
append avec une chaîne crée un nœud texte : le texte de l'avis n'est jamais interprété. En prime, replaceChildren évite le += dans la boucle. Le serveur, lui, doit aussi valider les avis et envoyer un en-tête CSP.
Tu avais trouvé ?
Exercice 3 sur 4 Classe conditionnelle Réussi À revoir
Écris la fonction marquerAlertes(tbody, pieces) qui parcourt les lignes du tbody (une ligne par pièce, dans le même ordre que le tableau pieces) et donne la classe alerte aux lignes des pièces en alerte, en la retirant aux autres.
Voir la réponseMasquer la réponse de l'exercice 3
function marquerAlertes(tbody, pieces) {
const lignes = tbody.querySelectorAll('tr');
lignes.forEach((tr, i) => {
const p = pieces[i];
tr.classList.toggle('alerte', p.quantite <= p.seuil);
});
}
toggle avec un deuxième argument ajoute la classe si la condition est vraie et la retire sinon : une seule ligne au lieu d'un if…else.
Tu avais trouvé ?
Exercice 4 sur 4 Une carte par pièce Réussi À revoir
Sans utiliser innerHTML, écris une fonction cartePiece(piece) qui renvoie cet élément, rempli avec les données de la pièce :
<article class="carte">
<h3>SSD NVMe 512 Go</h3>
<p>Référence : SSD-512</p>
<p class="stock">12 en stock</p>
</article>
La classe stock doit devenir stock vide quand la quantité vaut 0.
Voir la réponseMasquer la réponse de l'exercice 4
function cartePiece(piece) {
const article = document.createElement('article');
article.className = 'carte';
const titre = document.createElement('h3');
titre.textContent = piece.nom;
const ref = document.createElement('p');
ref.textContent = `Référence : ${piece.ref}`;
const stock = document.createElement('p');
stock.classList.add('stock');
if (piece.quantite === 0) stock.classList.add('vide');
stock.textContent = `${piece.quantite} en stock`;
article.append(titre, ref, stock);
return article;
}
document.querySelector('#cartes').replaceChildren(...pieces.map(cartePiece));
Tu avais trouvé ?