Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 querySelector et querySelectorAll ;
  • modifier un texte avec textContent et dire pourquoi innerHTML expose à 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 :

html
<!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>
documentle point d’entrée de tout scripthtmlheadtitle« Gestock : le stock de Réparinfo »bodydocument.bodyheaderh1« Gestock »nœud textep#resumemaintabletbody#liste-piecesquerySelector('#liste-pieces')tr.alertecreateElement('tr')td« RAM-16 »textContent
L'arbre DOM de la page : tbody est vide dans le HTML, JavaScript y ajoute les lignes

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.

js
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éthodeRenvoieSi rien ne correspond
document.querySelector('#resume')Le premier élémentnull
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 correspondnull

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 écritUsage
textContentLe texte brut, balises ignorées à la lecture, jamais interprétées à l'écritureAfficher une donnée : le choix par défaut
innerHTMLLe code HTML, interprété à l'écritureInsérer du balisage que tu as écrit, jamais une donnée
valueLa valeur d'un champ de formulaire (toujours une chaîne)Lire ou préremplir un input, select, textarea
checkedL'état d'une case à cocher (booléen)Lire une case
innerTextLe texte visible, selon le CSSRarement utile : plus lent que textContent
js
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>.

js
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 :

  1. Affiche les données avec textContent, jamais avec innerHTML. Une donnée, c'est tout ce qui vient de l'utilisateur, de l'URL, de localStorage ou d'une API, même la tienne.
  2. Construis la structure avec createElement (section suivante), et remplis chaque élément avec textContent.
  3. 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.
  4. 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 utiliser innerHTML.
js
// À 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.

js
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
css
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 :

js
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 :

html
<button data-action="retirer" data-ref="SSD-512">−</button>
js
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

js
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éthodeEffet
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.

html
<template id="modele-ligne">
  <tr><td class="ref"></td><td class="nom"></td><td class="quantite"></td></tr>
</template>
js
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éthodeDonne
element.parentElementLe parent
element.childrenLes enfants éléments (sans les nœuds texte)
element.firstElementChild, lastElementChildLe premier, le dernier enfant
element.nextElementSiblingL'élément suivant au même niveau
element.closest('sélecteur')Le plus proche ancêtre correspondant, lui-même compris
ligne.cellsLes 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 :

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 :

  1. le paragraphe d'identifiant resume ;
  2. toutes les lignes du tbody d'identifiant liste-pieces ;
  3. le champ de formulaire dont l'attribut name vaut prix ;
  4. le premier bouton dont l'attribut data-ref vaut CHG-65 ;
  5. 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
js
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.

js
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.

js
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
js
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 :

html
<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
js
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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Événements et formulaires : addEventListener, validation et regexRevoir le sommaire du cours