Pour garder les gestionnaires de clic actifs après le remplacement du contenu DOM d’une vue, attachez un seul écouteur à un élément parent qui subsiste après la mise à jour. Ce guide montre comment mettre en œuvre et vérifier cette approche dans Safari sur Mac à l’aide d’un petit sélecteur de tuiles synthétique.
La tâche est précise : reconstruire le contenu d’une vue JavaScript tout en gardant ses boutons réactifs et sa sélection intacte. Vous avez besoin de connaissances de base en JavaScript et d’un navigateur Safari à jour. L’exemple utilise les API standard du navigateur et ne nécessite ni framework ni installation de paquet.
Pourquoi les clics ne fonctionnent-ils plus après le remplacement du DOM ?
Un écouteur enregistré avec addEventListener() appartient à la cible de l’événement sur laquelle il a été enregistré. L’affectation à innerHTML d’un conteneur analyse le balisage fourni et remplace ses descendants. Par conséquent, un bouton nouvellement créé n’hérite pas d’un écouteur attaché au bouton précédent, même si son ID et son libellé sont identiques. Consultez la référence MDN de innerHTML et le modèle d’écouteurs d’événements du standard DOM.
Il faut distinguer le remplacement du contenu d’un conteneur du remplacement du conteneur lui-même. Si votre code conserve le conteneur et ne reconstruit que ses enfants, un écouteur placé sur ce conteneur peut rester en place. Si un rendu externe remplace aussi le conteneur, vous devez choisir un ancêtre qui subsiste ou attacher un écouteur lors de l’initialisation du nouveau conteneur.
Pour les événements de clic ordinaires qui remontent dans le DOM, la délégation d’événements permet à un parent de gérer les interactions provenant de ses descendants. Au lieu d’associer un écouteur à chaque bouton de remplacement, associez-le au parent stable et identifiez le bouton concerné à l’arrivée de l’événement. Le guide MDN sur la propagation des événements décrit cette approche.
Où placer l’écouteur ?
Commencez par repérer la limite exacte du DOM que votre opération de rendu remplace. Choisissez ensuite le parent approprié le plus proche, situé en dehors de cette limite. L’aide à la décision suivante applique aux modèles de mise à jour courants le comportement documenté du remplacement et des écouteurs. MDN : replaceChildren(), standard DOM : EventTarget.
| Votre mise à jour | Emplacement de l’écouteur | Exigence liée au cycle de vie |
|---|---|---|
| Reconstruire les enfants d’une racine de vue existante | Déléguer les clics à partir de cette racine | Enregistrer l’écouteur une fois à la création de la racine |
| Remplacer aussi la racine de la vue | Déléguer à partir d’un conteneur extérieur qui subsiste, ou associer l’écouteur à la nouvelle racine | Nettoyer l’enregistrement de la vue précédente |
| Créer des boutons nécessitant des gestionnaires individuels | Associer un écouteur à chaque nouveau bouton lors de sa création | Terminer l’association avant de rendre les nouveaux contrôles disponibles |
| Recharger le document entier | Initialiser les écouteurs dans le nouveau document | Relancer l’initialisation normale de la page |
Dans l’exemple ci-dessous, la structure reste une racine de vue contenant un titre, un état, un ensemble ordonné de choix de tuiles et un contrôle d’actualisation. Chaque rendu reconstruit ces enfants dans le même ordre. Seuls la racine de la vue, l’écouteur et l’état JavaScript persistent.
Créer une vue complète résistant au remplacement dans Safari
Préparez d’abord un onglet de navigateur temporaire. Dans Safari sur votre Mac, ouvrez Settings…, sélectionnez Advanced et activez Show features for web developers. Ouvrez l’inspecteur Web via Develop → Show Web Inspector, ou appuyez sur Option-Command-I. Ces étapes sont documentées dans le guide de WebKit sur l’activation de l’inspecteur Web.
Accédez à about:blank, ouvrez l’inspecteur Web et sélectionnez Console. Collez le programme complet ci-dessous dans son invite, puis exécutez-le. WebKit indique que l’invite de la console évalue le JavaScript dans le contexte d’exécution sélectionné de la page inspectée. Utilisez l’onglet vierge afin que la démonstration reste séparée de votre application.
Il s’agit d’un programme complet pour navigateur : il crée son propre DOM, définit ses données d’exemple, monte la vue et expose une méthode de nettoyage. Il remplace les enfants de la vue avec replaceChildren(), qui accepte des objets nœuds et remplace l’ensemble des enfants existants. La référence MDN de cette méthode décrit cette opération.
(() => {
window.domClickDemo?.destroy();
const root = document.createElement("section");
root.id = "dom-click-demo";
const controller = new AbortController();
const items = [
{ id: "a", label: "Tile A" },
{ id: "b", label: "Tile B" },
{ id: "c", label: "Tile C" }
];
const state = { selected: "None", refreshes: 0 };
function makeButton(action, label, id) {
const button = document.createElement("button");
button.type = "button";
button.dataset.action = action;
if (id !== undefined) button.dataset.id = id;
const caption = document.createElement("span");
caption.textContent = label;
button.append(caption);
return button;
}
function render() {
const heading = document.createElement("h2");
heading.textContent = "Tile picker";
const status = document.createElement("p");
status.textContent =
`Selected: ${state.selected}; refreshes: ${state.refreshes}`;
const list = document.createElement("ul");
for (const item of items) {
const row = document.createElement("li");
row.append(makeButton("select", item.label, item.id));
list.append(row);
}
const refresh = makeButton("refresh", "Replace view content");
root.replaceChildren(heading, status, list, refresh);
}
function handleClick(event) {
if (!(event.target instanceof Element)) return;
const button = event.target.closest("button[data-action]");
if (!button || !root.contains(button) || button.disabled) return;
if (button.dataset.action === "select") {
const item = items.find(item => item.id === button.dataset.id);
if (!item) return;
state.selected = item.label;
} else if (button.dataset.action === "refresh") {
state.refreshes += 1;
} else {
return;
}
render();
}
root.addEventListener("click", handleClick, {
signal: controller.signal
});
render();
document.body.append(root);
window.domClickDemo = {
root,
destroy() {
controller.abort();
root.remove();
}
};
})();
L’écouteur est enregistré en dehors de render(). Chaque sélection et actualisation crée de nouveaux boutons, mais tous restent sous la racine d’origine. L’objet d’état se trouve également en dehors du sous-arbre rendu : le remplacement du paragraphe d’état ne réinitialise donc ni la tuile sélectionnée ni le nombre d’actualisations.
La légende imbriquée est intentionnelle. Un clic sur le texte d’un bouton peut cibler son élément enfant. closest("button[data-action]") recherche le contrôle correspondant sur la cible et ses ancêtres, ce qui évite qu’un gestionnaire ne fonctionne que lorsque le bouton lui-même est la cible de l’événement. La référence MDN de closest() décrit cette recherche et le résultat null obtenu lorsqu’aucun élément ne correspond.
La vérification de contenance maintient le contrôle correspondant à l’intérieur de la limite de cette vue. Le standard DOM définit contains() selon que le nœud fourni est le nœud lui-même ou l’un de ses descendants. Standard DOM : Node.contains().
Distinguer l’initialisation, le rendu et le nettoyage
Cet exemple comporte trois responsabilités explicites. L’initialisation crée la racine et enregistre son écouteur. Le rendu met à jour les enfants de la racine à partir de l’état. Le nettoyage supprime l’écouteur et la racine. Cette répartition définit le cycle de vie de l’implémentation ; conservez les responsabilités équivalentes lorsque vous l’adaptez à votre application.
L’écouteur reçoit un AbortSignal et destroy() interrompt son contrôleur. Cette interruption supprime l’enregistrement associé, comme le décrit la référence MDN de addEventListener(). Relancer le programme complet commence par nettoyer la démonstration précédente, ce qui évite l’accumulation de plusieurs vues de démonstration.
Ne placez pas un nouvel écouteur anonyme dans chaque rendu. Les expressions de fonction anonymes répétées créent des objets fonction distincts : leurs enregistrements peuvent donc s’accumuler sur une racine qui subsiste. Réutiliser le même rappel, le même type d’événement et le même paramètre de capture évite un enregistrement en double, mais réserver l’association à l’initialisation rend la gestion des écouteurs plus claire. MDN : addEventListener(), standard DOM : ajout d’écouteurs.
Vérifier les remplacements répétés, la sélection et le nettoyage
Suivez cette séquence pour tester l’exemple. Les résultats attendus découlent des mises à jour explicites de l’état du programme ; ils ne signifient pas qu’un test dans Safari a été effectué.
- Cliquez sur
Tile B. L’état doit afficherSelected: Tile B; refreshes: 0. - Cliquez trois fois sur
Replace view content. L’état doit afficherSelected: Tile B; refreshes: 3. - Cliquez sur le texte de
Tile C. L’état doit afficherSelected: Tile C; refreshes: 3. - Cliquez sur le titre ou le paragraphe d’état. Aucune des deux valeurs ne doit changer.
- Exécutez
window.domClickDemo.destroy()dans la console. La démonstration doit disparaître. - Exécutez à nouveau le programme complet. Une seule nouvelle vue doit apparaître avec
Selected: None; refreshes: 0.
Les trois premiers contrôles portent sur le remplacement, la conservation de l’état et les clics sur les éléments imbriqués. Le quatrième vérifie le filtrage des actions. Les deux derniers vérifient le nettoyage et le parcours d’initialisation. Ensemble, ils mettent à l’épreuve le cycle de vie réel au lieu de confirmer seulement que les boutons initiaux fonctionnent.
La syntaxe JavaScript du programme a été vérifiée avec Node et un test distinct et déterministe de EventTarget a confirmé la déduplication des rappels et leur suppression au moyen d’un signal d’interruption. Ce test n’a pas exécuté le rendu du DOM dans un navigateur ni les interactions dans Safari ; utilisez la séquence ci-dessus pour les valider sur votre Mac.
Que faire si le gestionnaire ne fonctionne toujours pas dans votre application ?
Cherchez le premier point où le comportement diverge. Limitez-vous à la portée réduite de l’exemple pendant que vous vérifiez la limite de remplacement, le chemin de l’événement et les mises à jour d’état de votre application.
| Symptôme | Première vérification | Correction appropriée |
|---|---|---|
| Les clics fonctionnent jusqu’à l’actualisation d’une vue externe | La racine de l’écouteur a-t-elle aussi été remplacée ? | Associer l’écouteur à la nouvelle racine lors de l’initialisation ou choisir un parent qui subsiste |
| Un clic est reçu, mais aucune action ne s’exécute | Le nouveau contrôle correspond-il toujours au sélecteur et à la valeur d’action ? | Préserver le sélecteur du contrôle et son contrat d’action |
| Le comportement diffère lorsqu’on clique sur le texte du bouton | Le gestionnaire vérifie-t-il uniquement la cible exacte ? | Trouver le contrôle avec closest() |
| Un clic met l’état à jour plusieurs fois | Plusieurs enregistrements ou des gestionnaires directs et délégués sont-ils actifs simultanément ? | Confier la gestion de l’action à un seul parcours d’initialisation |
| Le gestionnaire s’exécute, mais l’affichage est réinitialisé | L’état est-il recréé pendant le rendu ? | Conserver l’état requis en dehors du sous-arbre remplacé |
Examinez également la propagation. Un gestionnaire descendant qui appelle stopPropagation() peut empêcher un événement en phase de remontée d’atteindre votre écouteur délégué. MDN explique à la fois l’arrêt de la propagation et la délégation d’événements. Vérifiez pourquoi la propagation est interrompue avant de modifier les phases d’écoute, car leur changement modifie aussi l’ordre d’exécution.
Si vous devez utiliser des écouteurs directs sur les boutons, associez-les à chaque fois que vous créez des boutons de remplacement. Une recherche et une association effectuées une seule fois ne peuvent pas initialiser les nœuds créés par les rendus ultérieurs : les enregistrements appartiennent à leurs cibles d’événement. De même, la délégation ne peut pas préserver un écouteur sur une racine supprimée par votre moteur de rendu. Ces limites découlent du modèle d’écouteurs du standard DOM.
Pour cette tâche, la disposition durable consiste à utiliser une racine de vue stable, un seul écouteur délégué, un état conservé en dehors des enfants rendus et un parcours de nettoyage explicite. Repérez les nœuds qui subsistent après chaque mise à jour, placez l’écouteur en conséquence et vérifiez plusieurs remplacements consécutifs avant d’intégrer la modification à la vue complète.
Sources
- Activation de l’inspecteur Web — WebKit.
- Interface de l’inspecteur Web — WebKit.
- Élément : propriété innerHTML — MDN Web Docs.
- Propagation des événements — MDN Web Docs.
- Élément : méthode replaceChildren() — MDN Web Docs.
- Élément : méthode closest() — MDN Web Docs.
- EventTarget : méthode addEventListener() — MDN Web Docs.
- Standard DOM — WHATWG.