Manipuler le DOM en JavaScript : sélectionner, modifier, créer des éléments

Vous avez une liste HTML et vous voulez en changer le premier élément, ajouter une ligne, puis en retirer une. JavaScript travaille pour cela sur le DOM : la représentation de la page sous forme de nœuds reliés entre eux. Le fichier HTML fournit le point de départ ; le DOM est ce que le script peut modifier pendant que la page est ouverte.

Nous allons suivre une seule liste, du sélecteur à la suppression. Si les balises de départ vous sont encore peu familières, relisez les bases pour débuter en HTML avant de passer au script.

Une page, deux lignes, un script chargé au bon moment

Créez index.html et dom.js dans le même dossier. Copiez cette page dans le premier fichier, puis assemblez les trois blocs JavaScript de la suite, dans leur ordre, dans le second. Ouvrez la page et la console des outils de développement pour lire les résultats.

<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Une liste et son DOM</title>
  <script src="dom.js" defer></script>
</head>
<body>
  <h1>À apprendre</h1>
  <ul id="competences">
    <li>HTML</li>
    <li>CSS</li>
  </ul>
</body>
</html>

defer fait exécuter ce script classique externe après l'analyse du document HTML. Sans cette attente, un script placé dans le head pourrait chercher une liste que le navigateur n'a pas encore construite. Une autre possibilité est de placer la balise du script à la fin du body, après la liste. Ici, gardez defer.

documentul#competencesli : HTMLli : CSS
Arbre simplifié : les nœuds intermédiaires et les nœuds de texte ne sont pas tous représentés.

Sélectionner un élément, et regarder ce qui manque

document représente le document ouvert. Sa méthode querySelector prend un sélecteur CSS et renvoie le premier élément correspondant, ou null si elle ne trouve rien. On peut aussi chercher à partir d'un élément déjà obtenu : ci-dessous, liste.querySelector limite la recherche à ses descendants.

const liste = document.querySelector("#competences");
if (liste === null) {
  throw new Error("Liste #competences absente");
}
const premier = liste.querySelector("li");
console.log("premier : " + premier.textContent);

const absent = document.querySelector("#inexistant");
console.log("absent : " + absent);
try {
  absent.textContent = "Impossible";
} catch (erreur) {
  console.log("erreur : " + erreur.name);
}
if (absent !== null) {
  absent.textContent = "Élément trouvé";
} else {
  console.log("correction : élément absent, écriture évitée");
}

Ce bloc provoque volontairement une vraie erreur, puis montre le contrôle qui l'évite. Voici les lignes obtenues lors du test du 9 octobre 2026 :

premier : HTML
absent : null
erreur : TypeError
correction : élément absent, écriture évitée

Le nom TypeError est relevé par le script ; le texte complet de l'exception dépend du moteur. Vérifier null ne répare pas un identifiant mal écrit. Si la liste est obligatoire, une erreur explicite comme celle du début signale le problème ; si l'élément est facultatif, on peut passer son traitement. Le premier li existe ici parce que le HTML fourni contient deux lignes.

Écrire du texte, sans le transformer en balises

La propriété textContent lit le texte des descendants et, lorsqu'on lui affecte une valeur, remplace les enfants par du texte. Les caractères qui ressemblent à du HTML restent donc du texte. innerHTML, au contraire, analyse la chaîne comme du balisage HTML. Le bloc suivant compare les deux sur un élément détaché de la page, puis renomme la première ligne de la liste.

const essai = document.createElement("li");
essai.textContent = "<strong>JavaScript</strong>";
console.log("textContent : " + essai.textContent);
console.log("balises après textContent : " + essai.children.length);
essai.innerHTML = "<strong>JavaScript</strong>";
console.log("balise après innerHTML : " + essai.firstElementChild.tagName);
premier.textContent = "HTML sémantique";
textContent : <strong>JavaScript</strong>
balises après textContent : 0
balise après innerHTML : STRONG

La chaîne de cet essai est écrite par nous, dans le code. Pour afficher une valeur venant d'un utilisateur ou d'une API, préférez textContent : l'insérer directement dans innerHTML peut permettre une injection HTML et une attaque XSS. Ce test constate l'analyse des balises, il ne constitue pas un test de protection contre toutes les attaques. Si vous avez besoin de structure, créez les éléments explicitement, comme dans la section suivante.

Créer, ajouter, retirer : les références restent-elles à jour ?

querySelectorAll renvoie une NodeList statique : la sélection garde les références trouvées lors de l'appel. getElementsByTagName renvoie ici une HTMLCollection live, dont le contenu suit les modifications de la liste. Prenons les deux avant d'ajouter un élément pour voir la différence.

const selection = liste.querySelectorAll("li");
const collection = liste.getElementsByTagName("li");
const nouveau = document.createElement("li");
nouveau.textContent = "JavaScript";
console.log("avant append : " + nouveau.isConnected);
liste.append(nouveau);
console.log("après append : " + nouveau.isConnected);
console.log("tailles après ajout : " + selection.length + " / " + collection.length);

selection.forEach((element) => element.classList.add("repere"));
console.log("nouveau repéré : " + nouveau.classList.contains("repere"));
selection[1].remove();
console.log("tailles après retrait : " + selection.length + " / " + collection.length);
console.log("ancien CSS connecté : " + selection[1].isConnected);
console.log("liste finale : " + Array.from(liste.querySelectorAll("li"),
  (element) => element.textContent).join(" | "));
avant append : false
après append : true
tailles après ajout : 2 / 3
nouveau repéré : false
tailles après retrait : 2 / 2
ancien CSS connecté : false
liste finale : HTML sémantique | JavaScript

createElement fabrique un élément ; append le rattache à la liste. Le nouveau li ne reçoit pas la classe repere, parce que la boucle parcourt la sélection prise avant son ajout. Après remove, cette sélection contient toujours la référence vers l'ancien élément CSS, désormais détaché. « Statique » ne signifie donc pas que les éléments eux-mêmes sont gelés : la première référence a bien reçu sa nouvelle classe.

Pour traiter tous les éléments présents après un ajout, refaites la sélection à ce moment-là. Une collection live peut être utile pour lire un nombre actualisé ; la modifier pendant une boucle demande en revanche de surveiller les indices, puisque les éléments peuvent se décaler. Ici, la suppression s'appuie sur la sélection statique pour garder une cible précise.

La classe ajoutée n'a aucun effet visuel tant qu'une règle CSS ne la cible pas. Le but de cette page est de vérifier la manipulation du DOM. Pour utiliser ces opérations dans une interaction complète, le projet de todo list en TypeScript les replace dans une liste pilotée par l'utilisateur.

Ce que le banc a réellement vérifié

Les quatre blocs exécutables ont été extraits automatiquement de cet article : un fichier HTML, puis les trois blocs JavaScript concaténés dans dom.js. Les blocs de sortie ont été comparés aux journaux, ligne par ligne. Commande du banc : node mesures.mjs --article chemin/vers/manipuler-dom-javascript.md, après npm ci --ignore-scripts. Le mode sans navigateur utilise jsdom 26.1.0 : il vérifie les opérations DOM, pas l'affichage ni le chargement réel d'un script avec defer.

Le même HTML et le même script ont aussi été chargés dans Chromium 153.0.8010.12 avec Playwright 1.63.0, depuis Node 20.19.4 sous Windows, le 9 octobre 2026. Les quatorze lignes de console ci-dessus et la liste finale ont concordé dans les deux environnements. Le mode Chromium du banc utilise node mesures.mjs --chromium --article chemin/vers/manipuler-dom-javascript.md. Aucune mesure de vitesse ni comparaison de performance n'est déduite de ces essais.

Sources consultées le 9 octobre 2026 :