In den letzten Wochen hatte ich mal wieder Zeit ein wenig selbst zu programmieren und wollte ein wenig mit Höhen und Breiten von dynamisch generierten DOM-Elementen rechnen. Das Problem hierbei ist, dass die wahren Höhen und Breiten eines Elements erst feststehen, wenn es im Dokument gelandet ist. Also galt es, den Zeitpunkt des Einfügens eines Elements in das DOM abzupassen. Das klingt einfacher, als es ist, wenngleich es sich nach kurzer Überlegung dann doch recht simpel lösen ließ.

Das Problem ist, dass es mal ein Event namens DOMNodeInserted gab, das jedoch zusammen mit allen anderen Mutation Events aus den Standards geflogen ist. Als Ersatz sollen MutationObserver herhalten, doch einen direkten Weg zur Beobachtung des eingefügt-werdens gibt es nicht. Also nehmen wir den indirekten Weg:

  1. Wir setzen einen MutationObserver nicht auf unser Ziel-Element, sondern auf irgendein Element im Dokument an, das eines Tages (direkt oder indirekt) unser Element beinhalten wird. Das könnten z.B. document.body oder document.firstElementChild sein.
  2. Der Observer durchsucht bei childList-Ereignissen (d.h. Änderungen an den Kindelementen des observierten Elements) die neu hinzugefügten Knoten nach unserem Ziel-Element. Wird es gefunden, führen wir einen Callback aus.

Die einfachste Implementierung dieses Patterns sieht wie folgt aus:

function observeInsertion (targetNode, callback, rootNode = document.firstElementChild) {
  const insertionObserver = new MutationObserver( (mutations) => {
    for (const mutation of mutations) {
      if (mutation.type === "childList") {
        for (const added of mutation.addedNodes) {
          if (added === targetNode || added.contains(targetNode)) {
            callback(targetNode);
            insertionObserver.disconnect();
            return;
          }
        }
      }
    }
  });
  insertionObserver.observe(rootNode, {
    childList: true,
    subtree: true,
  });
}

In Aktion auf CodePen!

MutationObserver sammeln durch eine DOM-Änderung anfallende Mutations-Events ein und liefern sie im Paket an den Observations-Callback – mutations ist also ein Array von Objekten. Aus diesem picken wir uns in einer for-of-Schleife die childList-Events und durchsuchen die hinzugefügten DOM-Knoten nach unserem Ziel-Element. Da wir den gesamten DOM-Tree beobachten (subtree: true) wird uns das Einfügen unseres Ziel-Elements nicht entgehen, egal wie tief verschachtelt es stattfindet. Sobald wir das Einfügen unseres Elements beobachtet haben, triggern wir den Callback, schalten den MutationObserver mit disconnect() ab und steigen durch das return aus der dann fertig abgearbeiteten Observer-Callback-Funktion aus.

Dieser Code (bzw. seine TypeScript-Variante) haben für mich soweit ganz gut funktioniert. Vermutlich könnte man für den Einsatz auf breiter Front eine noch effizientere Variante basteln, die nicht für jede zu beobachtende Node einen eigenen Observer benötigt, aber im allerbesten Fall kommt man ganz ohne ein Element-Wurde-Eingefügt-Event aus. Sofern es nicht gerade um (wie in meinem Fall) unsauberes Gewurschtel mit Computed Styles geht, gibt es wirklich wenig Gründe, einen Ersatz für DOMNodeInserted überhaupt zu wollen, denn mit Event Delegation und anderen intelligenten Techniken ist es eigentlich egal, wann und ob ein Element im DOM gelandet ist.

Am Ende habe ich den in diesem Artikel gezeigten Mutation Observer selbst aus meinem Code geworfen und die CSS-Rechnerei in CSS-Variablen ausgelagert.