Laat klikhandlers werken nadat de DOM-inhoud van een weergave is vervangen door één listener te koppelen aan een bovenliggend element dat de update overleeft. Deze handleiding laat zien hoe je dit patroon implementeert en controleert in Safari op een Mac met een kleine, synthetische tegelkiezer.

De taak is specifiek: bouw de inhoud van een JavaScript-weergave opnieuw op terwijl de knoppen responsief blijven en de selectie behouden blijft. Je hebt basiskennis van JavaScript en een recente Safari-browser nodig. Het voorbeeld gebruikt standaard browser-API's en vereist geen framework of pakketinstallatie.

Waarom werken klikken niet meer na een DOM-vervanging?

Een listener die met addEventListener() is geregistreerd, hoort bij het gebeurtenisdoel waarop deze is geregistreerd. Een toewijzing aan innerHTML van een container parseert de aangeleverde markup en vervangt de onderliggende elementen. Een nieuw aangemaakte knop erft daardoor geen listener die aan de vorige knop was gekoppeld, zelfs niet als de ID en het label overeenkomen. Zie de referentie van innerHTML op MDN en het eventlistenermodel van de DOM Standard.

Het nuttige onderscheid is dat tussen de inhoud van een container vervangen en de container zelf vervangen. Als je code de container behoudt en alleen de onderliggende elementen opnieuw opbouwt, kan een listener op die container blijven staan. Als een buitenliggende render ook de container vervangt, moet je een bovenliggend element kiezen dat blijft bestaan, of een listener koppelen tijdens de initialisatie van de nieuwe container.

Bij gewone klikgebeurtenissen die omhoog borrelen, kan een bovenliggend element met eventdelegatie interacties afhandelen die in onderliggende elementen beginnen. Koppel de listener aan het stabiele bovenliggende element in plaats van aan elke vervangende knop, en bepaal welk deel van de knop relevant is zodra de gebeurtenis binnenkomt. De handleiding van MDN over het omhoog borrelen van gebeurtenissen beschrijft dit patroon.

Waar moet de listener staan?

Bepaal eerst precies welke DOM-grens je renderbewerking vervangt. Kies daarna het meest specifieke geschikte bovenliggende element buiten die grens. De volgende besliswijzer past het beschreven vervangings- en listenergedrag toe op veelvoorkomende updatepatronen. MDN: replaceChildren(), DOM Standard: EventTarget.

Je update Plaatsing van de listener Vereiste voor de levenscyclus
De onderliggende elementen van een bestaande weergaveroot opnieuw opbouwen Klikken delegeren vanaf die root Eenmalig registreren wanneer de root wordt aangemaakt
Ook de weergaveroot vervangen Delegeren vanaf een buitenliggende container die blijft bestaan, of de nieuwe root gebruiken De registratie van de vorige weergave opruimen
Knoppen maken die afzonderlijke handlers nodig hebben Elke nieuwe knop tijdens het opbouwen koppelen Het koppelen voltooien voordat de nieuwe bedieningselementen beschikbaar worden
Het hele document opnieuw laden Listeners in het nieuwe document initialiseren De normale paginainitialisatie opnieuw uitvoeren

Voor het onderstaande voorbeeld blijft de structuur een weergaveroot met een kop, status, geordende set tegelopties en een vernieuwknop. Elke render bouwt deze onderliggende elementen opnieuw op in dezelfde volgorde. Alleen de weergaveroot, listener en JavaScript-status blijven behouden.

Bouw een volledige vervangingsbestendige weergave in Safari

Bereid eerst een tijdelijk browsertabblad voor. Open in Safari op je Mac Instellingen…, selecteer Geavanceerd en schakel Toon functies voor webontwikkelaars in. Open Web Inspector via Ontwikkel → Toon Web Inspector, of druk op Option-Command-I. Dit zijn de stappen die worden beschreven in WebKit's handleiding voor het inschakelen van Web Inspector.

Ga naar about:blank, open Web Inspector en selecteer Console. Plak het volledige onderstaande programma in de prompt en voer het uit. WebKit vermeldt dat de Console Prompt JavaScript uitvoert in de geselecteerde uitvoeringscontext van de geïnspecteerde pagina. Gebruik het lege tabblad zodat de demonstratie gescheiden blijft van je applicatie.

Dit is een volledig browserprogramma: het maakt zijn eigen DOM en voorbeeldgegevens, koppelt de weergave aan het document en biedt een opruimmethode. Het vervangt de onderliggende elementen van de weergave met replaceChildren(), dat nodeobjecten accepteert en de bestaande verzameling onderliggende elementen vervangt. De methodereferentie van MDN beschrijft deze bewerking.

(() => {
  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();
    }
  };
})();

De listener wordt buiten render() geregistreerd. Elke selectie en vernieuwing maakt nieuwe knoppen, maar al die knoppen blijven onder de oorspronkelijke root staan. Het statusobject staat ook buiten de gerenderde subtree, zodat het vervangen van de statusparagraaf de geselecteerde tegel of het aantal vernieuwingen niet reset.

Het geneste bijschrift staat er met opzet. Als je op de tekst van een knop klikt, kan het onderliggende element het gebeurtenisdoel zijn. closest("button[data-action]") zoekt in het doel en de bovenliggende elementen naar het overeenkomende bedieningselement, zodat de handler ook werkt wanneer de knop zelf niet het gebeurtenisdoel is. De referentie van closest() op MDN beschrijft de zoekactie en het resultaat null wanneer er geen overeenkomst is.

De insluitingscontrole houdt het overeenkomende bedieningselement binnen de grens van deze weergave. De DOM Standard definieert contains() aan de hand van de vraag of de opgegeven node de node zelf is of een van de onderliggende nodes ervan. DOM Standard: Node.contains().

Houd initialisatie, rendering en opruimen gescheiden

Dit voorbeeld heeft drie expliciete verantwoordelijkheden. De initialisatie maakt de root en registreert de listener. De render werkt de onderliggende elementen van de root bij op basis van de status. Het opruimen verwijdert de listener en de root. Die verdeling vormt het levenscycluscontract van de implementatie; behoud gelijkwaardige verantwoordelijkheden wanneer je dit aan je applicatie aanpast.

De listener ontvangt een AbortSignal, en destroy() breekt de controller af. Dit verwijdert de bijbehorende registratie, zoals beschreven in de referentie van addEventListener() op MDN. Als je het volledige programma opnieuw uitvoert, wordt de vorige demonstratie eerst opgeruimd, zodat er niet meerdere demoweergaven ontstaan.

Plaats niet bij elke render een nieuwe anonieme listener. Herhaalde anonieme functie-expressies maken verschillende functieobjecten, waardoor registraties zich kunnen opstapelen op een root die blijft bestaan. Hergebruik van dezelfde callback, hetzelfde gebeurtenistype en dezelfde capture-instelling voorkomt een dubbele registratie, maar als je het koppelen in de initialisatie houdt, is duidelijker wie ervoor verantwoordelijk is. MDN: addEventListener(), DOM Standard: listeners toevoegen.

Controleer herhaalde vervanging, selectie en opruiming

Gebruik deze reeks om het voorbeeld te testen. De verwachte resultaten volgen uit de expliciete statusupdates van het programma; ze zijn geen bewering dat de test in Safari is uitgevoerd.

  1. Klik op Tegel B. De status moet Selected: Tile B; refreshes: 0 weergeven.
  2. Klik drie keer op Vervang weergave-inhoud. De status moet Selected: Tile B; refreshes: 3 weergeven.
  3. Klik op de tekst van Tegel C. De status moet veranderen in Selected: Tile C; refreshes: 3.
  4. Klik op de kop of de statusparagraaf. Geen van beide waarden mag veranderen.
  5. Voer window.domClickDemo.destroy() uit in de Console. De demonstratie moet verdwijnen.
  6. Voer het volledige programma opnieuw uit. Er moet één nieuwe weergave verschijnen met Selected: None; refreshes: 0.

De eerste drie controles behandelen vervanging, behouden status en geneste klikdoelen. De vierde controleert de filter voor acties. De laatste twee controleren het opruim- en initialisatiepad. Samen testen ze de daadwerkelijke levenscyclus in plaats van alleen te bevestigen dat de knoppen aanvankelijk werken.

De JavaScript-syntaxis van het programma is gecontroleerd met Node, en een afzonderlijke deterministische EventTarget-controle bevestigde dat callbacks worden ontdubbeld en via een abortsignaal worden verwijderd. In die controle zijn de DOM-rendering in de browser en interacties in Safari niet uitgevoerd; gebruik de bovenstaande reeks om ze op je Mac te valideren.

Wat als de handler in je applicatie nog steeds niet werkt?

Zoek het eerste punt waarop het gedrag afwijkt. Houd de beperkte scope van het voorbeeld aan terwijl je de vervangingsgrens, het gebeurtenispad en de statusupdates van je applicatie controleert.

Symptoom Controleer eerst Passende correctie
Klikken werkt totdat een buitenliggende weergave wordt vernieuwd Is de root van de listener ook vervangen? Koppel de nieuwe root tijdens de initialisatie of kies een bovenliggend element dat blijft bestaan
Er komt een klik binnen, maar er wordt geen actie uitgevoerd Voldoet het nieuwe bedieningselement nog aan de selector en actiewaarde? Behoud de selector en het actiecontract van het bedieningselement
Klikken op knoptekst gedraagt zich anders Controleert de handler alleen het exacte doel? Zoek het bedieningselement met closest()
Eén klik werkt de status meerdere keren bij Zijn er meerdere registraties actief, of zowel directe als gedelegeerde handlers? Wijs de actie toe aan één initialisatiepad
De handler wordt uitgevoerd, maar de weergave wordt gereset Wordt de status opnieuw aangemaakt tijdens het renderen? Houd de vereiste status buiten de vervangen subtree

Controleer ook de propagatie. Een handler op een onderliggend element die stopPropagation() aanroept, kan voorkomen dat een omhoog borrelende gebeurtenis je gedelegeerde listener bereikt. MDN legt zowel het stoppen van propagatie als eventdelegatie uit. Controleer waarom de propagatie wordt gestopt voordat je de fasen van listeners wijzigt, omdat een wijziging van de fasen ook de uitvoeringsvolgorde verandert.

Als je directe knoplisteners moet gebruiken, koppel ze dan telkens wanneer je vervangende knoppen maakt. Een eenmalige query en koppelronde kan geen nodes initialiseren die bij latere renders worden aangemaakt: registraties horen bij hun gebeurtenisdoelen. Eventdelegatie kan evenmin een listener behouden op een root die je renderer weggooit. Deze grenzen volgen uit het listenermodel van de DOM Standard.

Voor deze taak bestaat de blijvende oplossing uit een stabiele weergaveroot, één gedelegeerde listener, status buiten de gerenderde onderliggende elementen en een expliciet opruimpad. Bepaal welke nodes elke update overleven, plaats de listener dienovereenkomstig en controleer meerdere opeenvolgende vervangingen voordat je de wijziging in de grotere weergave integreert.

Bronnen