Praktische Anwendungsfälle für die closest()-Methode von JavaScript 

Avatar of Andreas Remdt
Andreas Remdt am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Hatten Sie jemals das Problem, den Elternknoten eines DOM-Knotens in JavaScript zu finden, aber Sie sind sich nicht sicher, wie viele Ebenen Sie nach oben traversieren müssen, um dorthin zu gelangen? Betrachten wir zum Beispiel dieses HTML

<div data-id="123">
  <button>Click me</button>
</div>

Das ist ziemlich einfach, oder? Sagen wir, Sie möchten den Wert von data-id abrufen, nachdem ein Benutzer auf die Schaltfläche klickt

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});

In diesem sehr Fall reicht die Node.parentNode API aus. Was sie tut, ist, den übergeordneten Knoten eines gegebenen Elements zurückzugeben. Im obigen Beispiel ist evt.target die geklickte Schaltfläche; ihr übergeordneter Knoten ist das Div mit dem Datenattribut.

Aber was ist, wenn die HTML-Struktur tiefer verschachtelt ist als das? Sie könnte sogar dynamisch sein, abhängig von ihrem Inhalt.

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>

Unsere Aufgabe wurde gerade erheblich schwieriger, indem wir ein paar HTML-Elemente hinzugefügt haben. Sicher, wir könnten etwas wie element.parentNode.parentNode.parentNode.dataset.id tun, aber komm schon... das ist nicht elegant, wiederverwendbar oder skalierbar.

Der alte Weg: Mit einer while-Schleife

Eine Lösung wäre, eine while-Schleife zu verwenden, die läuft, bis der übergeordnete Knoten gefunden wurde.

function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}

Wenn wir wieder dasselbe HTML-Beispiel von oben verwenden, würde es so aussehen

var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"

Diese Lösung ist alles andere als perfekt. Stellen Sie sich vor, Sie möchten IDs oder Klassen oder eine andere Art von Selektor verwenden, anstelle des Tag-Namens. Zumindest ermöglicht sie eine variable Anzahl von Kindknoten zwischen dem übergeordneten Element und unserer Quelle.

Es gibt auch jQuery

Früher, wenn man sich nicht mit dem Schreiben der oben genannten Funktion für jede Anwendung auseinandersetzen wollte (und seien wir ehrlich, wer will das?), dann war eine Bibliothek wie jQuery nützlich (und das ist sie immer noch). Sie bietet eine .closest()-Methode genau dafür

$("button").closest("[data-id='123']")

Der neue Weg: Mit Element.closest()

Auch wenn jQuery immer noch ein valider Ansatz ist (hey, manche von uns sind daran gebunden), ist es für diese eine Methode zu viel, sie einem Projekt hinzuzufügen, besonders wenn man das Gleiche mit nativem JavaScript haben kann.

Und da kommt Element.closest ins Spiel

var button = document.querySelector("button");


console.log(button.closest("div"));
// prints the HTMLDivElement

Da haben wir es! So einfach kann es sein, und das ohne Bibliotheken oder zusätzlichen Code.

Element.closest() ermöglicht es uns, den DOM nach oben zu durchlaufen, bis wir ein Element finden, das dem angegebenen Selektor entspricht. Das Geniale ist, dass wir jeden Selektor übergeben können, den wir auch an Element.querySelector oder Element.querySelectorAll übergeben würden. Es kann eine ID, Klasse, ein Datenattribut, ein Tag oder was auch immer sein.

element.closest("#my-id"); // yep
element.closest(".some-class"); // yep
element.closest("[data-id]:not(article)") // hell yeah

Wenn Element.closest den übergeordneten Knoten basierend auf dem angegebenen Selektor findet, gibt er ihn genauso zurück wie  document.querySelector. Andernfalls, wenn es keinen übergeordneten Knoten findet, gibt es stattdessen null zurück, was die Verwendung mit if-Bedingungen erleichtert

var button = document.querySelector("button");


console.log(button.closest(".i-am-in-the-dom"));
// prints HTMLElement


console.log(button.closest(".i-am-not-here"));
// prints null


if (button.closest(".i-am-in-the-dom")) {
  console.log("Hello there!");
} else {
  console.log(":(");
}

Bereit für ein paar reale Beispiele? Los geht's!

Anwendungsfall 1: Dropdowns

Unser erstes Demo ist eine einfache (und bei weitem nicht perfekte) Implementierung eines Dropdown-Menüs, das sich nach dem Klicken auf eines der Menüeinträge der obersten Ebene öffnet. Beachten Sie, wie das Menü offen bleibt, selbst wenn Sie irgendwo innerhalb des Dropdowns klicken oder Text auswählen? Aber klicken Sie irgendwo außerhalb, und es schließt sich.

Die Element.closest API erkennt diesen Klick außerhalb. Das Dropdown selbst ist ein <ul>-Element mit der Klasse .menu-dropdown, sodass das Klicken irgendwo außerhalb des Menüs es schließt. Das liegt daran, dass der Wert für evt.target.closest(".menu-dropdown") null sein wird, da es keinen übergeordneten Knoten mit dieser Klasse gibt.

function handleClick(evt) {
  // ...
  
  // if a click happens somewhere outside the dropdown, close it.
  if (!evt.target.closest(".menu-dropdown")) {
    menu.classList.add("is-hidden");
    navigation.classList.remove("is-expanded");
  }
}

Innerhalb der Callback-Funktion handleClick entscheidet eine Bedingung, was zu tun ist: das Dropdown schließen. Wenn irgendwo anders innerhalb der ungeordneten Liste geklickt wird, findet und gibt Element.closest sie zurück, was dazu führt, dass das Dropdown geöffnet bleibt.

Anwendungsfall 2: Tabellen

Dieses zweite Beispiel rendert eine Tabelle, die Benutzerinformationen anzeigt, sagen wir als Komponente in einem Dashboard. Jeder Benutzer hat eine ID, aber anstatt sie anzuzeigen, speichern wir sie als Datenattribut für jedes <tr>-Element.

<table>
  <!-- ... -->
  <tr data-userid="1">
    <td>
      <input type="checkbox" data-action="select">
    </td>
    <td>John Doe</td>
    <td>[email protected]</td>
    <td>
      <button type="button" data-action="edit">Edit</button>
      <button type="button" data-action="delete">Delete</button>
    </td>
  </tr>
</table>

Die letzte Spalte enthält zwei Schaltflächen zum Bearbeiten und Löschen eines Benutzers aus der Tabelle. Die erste Schaltfläche hat das Datenattribut data-action von edit, und die zweite Schaltfläche ist delete. Wenn wir auf eine von beiden klicken, wollen wir eine Aktion auslösen (wie das Senden einer Anfrage an einen Server), aber dafür wird die Benutzer-ID benötigt.

Ein Klick-Ereignis-Listener wird an das globale Fensterobjekt angehängt, sodass jedes Mal, wenn der Benutzer irgendwo auf der Seite klickt, die Callback-Funktion handleClick aufgerufen wird.

function handleClick(evt) {
  var { action } = evt.target.dataset;
  
  if (action) {
    // `action` only exists on buttons and checkboxes in the table.
    let userId = getUserId(evt.target);
    
    if (action == "edit") {
      alert(`Edit user with ID of ${userId}`);
    } else if (action == "delete") {
      alert(`Delete user with ID of ${userId}`);
    } else if (action == "select") {
      alert(`Selected user with ID of ${userId}`);
    }
  }
}

Wenn ein Klick irgendwo anders als auf eine dieser Schaltflächen erfolgt, existiert kein data-action-Attribut, daher passiert nichts. Wenn Sie jedoch auf eine der Schaltflächen klicken, wird die Aktion bestimmt (das nennt man übrigens Event-Delegation), und als nächster Schritt wird die Benutzer-ID durch Aufruf von getUserId abgerufen.

function getUserId(target) {
  // `target` is always a button or checkbox.
  return target.closest("[data-userid]").dataset.userid;
}

Diese Funktion erwartet einen DOM-Knoten als einzigen Parameter und verwendet beim Aufruf Element.closest, um die Tabellenzeile zu finden, die die gedrückte Schaltfläche enthält. Sie gibt dann den Wert data-userid zurück, der nun verwendet werden kann, um eine Anfrage an einen Server zu senden.

Anwendungsfall 3: Tabellen in React

Bleiben wir beim Tabellenbeispiel und sehen wir uns an, wie wir damit in einem React-Projekt umgehen würden. Hier ist der Code für eine Komponente, die eine Tabelle zurückgibt

function TableView({ users }) {
  function handleClick(evt) {
    var userId = evt.currentTarget
    .closest("[data-userid]")
    .getAttribute("data-userid");


    // do something with `userId`
  }


  return (
    <table>
      {users.map((user) => (
        <tr key={user.id} data-userid={user.id}>
          <td>{user.name}</td>
          <td>{user.email}</td>
          <td>
            <button onClick={handleClick}>Edit</button>
          </td>
        </tr>
      ))}
    </table>
  );
}

Ich finde, dieser Anwendungsfall kommt häufig vor – es ist recht üblich, über eine Datenmenge zu iterieren und sie in einer Liste oder Tabelle anzuzeigen, und dann dem Benutzer zu ermöglichen, etwas damit zu tun. Viele Leute verwenden Inline-Pfeilfunktionen, so

<button onClick={() => handleClick(user.id)}>Edit</button>

Während dies auch eine gültige Methode zur Lösung des Problems ist, bevorzuge ich die Technik mit data-userid. Einer der Nachteile der Inline-Pfeilfunktion ist, dass React bei jeder Neurendering der Liste die Callback-Funktion neu erstellen muss, was zu einem möglichen Leistungsproblem bei großen Datenmengen führen kann.

In der Callback-Funktion behandeln wir einfach das Ereignis, indem wir das Ziel (die Schaltfläche) extrahieren und das übergeordnete <tr>-Element abrufen, das den data-userid-Wert enthält.

function handleClick(evt) {
  var userId = evt.target
  .closest("[data-userid]")
  .getAttribute("data-userid");


  // do something with `userId`
}

Anwendungsfall 4: Modals

Dieses letzte Beispiel ist eine weitere Komponente, die Sie sicher schon einmal gesehen haben: ein Modal. Modals sind oft schwierig zu implementieren, da sie viele Funktionen bieten müssen, während sie barrierefrei und (idealerweise) gut aussehend sind.

Wir möchten uns darauf konzentrieren, wie das Modal geschlossen wird. In diesem Beispiel ist dies möglich, indem entweder die Esc-Taste auf der Tastatur gedrückt wird, auf eine Schaltfläche im Modal geklickt wird oder irgendwo außerhalb des Modals geklickt wird.

In unserem JavaScript möchten wir auf Klicks irgendwo im Modal hören

var modal = document.querySelector(".modal-outer");

modal.addEventListener("click", handleModalClick);

Das Modal ist standardmäßig über eine Utility-Klasse .is-hidden ausgeblendet. Erst wenn ein Benutzer auf die große rote Schaltfläche klickt, öffnet sich das Modal, indem diese Klasse entfernt wird. Und sobald das Modal geöffnet ist, schließt ein Klick irgendwo darin – mit Ausnahme der Schließtaste – es nicht versehentlich. Die Callback-Funktion des Ereignis-Listeners ist dafür verantwortlich

function handleModalClick(evt) {
  // `evt.target` is the DOM node the user clicked on.
  if (!evt.target.closest(".modal-inner")) {
    handleModalClose();
  }
}

evt.target ist der geklickte DOM-Knoten, der in diesem Beispiel das gesamte Backdrop hinter dem Modal ist, <div class="modal-outer">. Dieser DOM-Knoten befindet sich nicht innerhalb von <div class="modal-inner">, daher kann Element.closest() so viel bubbeln, wie es will, und wird ihn nicht finden. Die Bedingung prüft dies und löst die Funktion handleModalClose aus.

Ein Klick irgendwo innerhalb des Modals, z. B. auf die Überschrift, würde <div class="modal-inner"> zum übergeordneten Knoten machen. In diesem Fall ist die Bedingung nicht wahrheitsgemäß, und das Modal bleibt im offenen Zustand.

Ach ja, und zur Browserunterstützung…

Wie bei jeder coolen "neuen" JavaScript-API ist die Browserunterstützung etwas, das man berücksichtigen muss. Die gute Nachricht ist, dass Element.closest nicht *so* neu ist und seit geraumer Zeit in allen wichtigen Browsern unterstützt wird, mit einer beeindruckenden Supportabdeckung von 94 %. Ich würde sagen, das qualifiziert es als sicher für die Verwendung in einer Produktionsumgebung.

Der einzige Browser, der keinerlei Unterstützung bietet, ist Internet Explorer (alle Versionen). Wenn Sie IE unterstützen müssen, sind Sie mit dem jQuery-Ansatz wahrscheinlich besser bedient.


Wie Sie sehen können, gibt es einige ziemlich solide Anwendungsfälle für Element.closest. Was Bibliotheken wie jQuery uns früher relativ einfach gemacht haben, kann jetzt nativ mit Vanilla JavaScript verwendet werden.

Dank der guten Browserunterstützung und der einfach zu bedienenden API bin ich stark auf diese kleine Methode in vielen Anwendungen angewiesen und wurde bisher nicht enttäuscht.

Haben Sie andere interessante Anwendungsfälle? Lassen Sie es mich gerne wissen.