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.
Ich habe
closestkürzlich verwendet, um eine ganze Produktkarte klickbar zu machen.Hier ist ein vereinfachtes Beispiel
Vielen Dank für das Teilen dieses Beispiels. Tatsächlich ist dies ein weiterer Anwendungsfall für diese Methode, und ich bin mir ziemlich sicher, dass es noch unzählige mehr gibt :)
Verwenden Sie kein map, mein Freund. Verwenden Sie forEach.
@Ackman
Als Antwort auf Ihren Kommentar „verwenden Sie
forEachstattmap“…Ich habe
mapnach sorgfältiger Überlegung gewählt – siehe https://codeburst.io/javascript-map-vs-foreach-f38111822c0fSchön... obwohl ich bei
forEachanstatt bei map bleiben würde, ja, ich weiß, map ist schneller als foreEach, aber der Zweck von map ist die Umwandlung von Daten in etwas anderes, während foreEach eher auf die Auslösung/Erzeugung von Nebeneffekten abzielt, wie z. B. Protokollierung oder, in Ihrem Fall, die klickbare Gestaltung der Karten.Warum jQuery anstelle eines Polyfills empfehlen? MDN hat eine schöne Polyfill-Funktion dafür.
Ja, MDN hat tatsächlich einen sehr schönen und prägnanten Polyfill dafür. Die Idee war jedoch zu zeigen, wie es war, bevor diese API aufkam, und damals war jQuery weit verbreitet (und ist es immer noch) und bot die gleiche Funktionalität.
Manche Leute mögen das interessant finden, aber die Absicht war nicht, eine Alternative oder einen Polyfill zu
Element.closestzu zeigen.Warum jQuery anstelle eines Polyfills empfehlen? MDN hat eine schöne Polyfill-Funktion dafür.
Vielen Dank für das Teilen dieses Beispiels. Tatsächlich ist dies ein weiterer Anwendungsfall für diese Methode, und ich bin mir ziemlich sicher, dass es noch unzählige mehr gibt....
Ich verwende es häufig in Google Tag Manager, um Ereignisse auf UI-Elementen zu verfolgen.
Ein gutes Beispiel ist, sagen wir, Sie haben ein rotierendes Banner mit einer CTA-Schaltfläche darin. Sie möchten wissen, welche Bannerposition die meiste Interaktion erhält. In GTM können Sie eine benutzerdefinierte Variable festlegen, um diese Position zu erhalten.
(Wir verwenden einen Rotator, der Duplikate des HTML des Slides erstellt, wenn er sich bewegt, sodass Sie keine Art von Geschwisterzählung für die Position verwenden können, daher das Datenattribut.)
Vielen Dank für das Teilen dieses interessanten Anwendungsfalls, Zach!
@Roy: Foreach in React? Nein, map ist die einzige gute Lösung. Innerhalb von JSX gibt es keine andere Lösung als map.
Ich denke, „window.addEventListener“ ist zu teuer.
In Fall 1 & 4 kann das Element mit „tabindex“ fokussiert werden.
Element.addEventListener(“blur“,()=>{
if(document.activeElement !== Element || document.activeElement !== childrens){
// etwas ausblenden
}
})
Noch cooler ist, dass Sie eine kommagetrennte Zeichenkette als Liste von Selektoren zum Abgleichen verwenden können.
element.closest('.foo, .bar')gleicht Elemente mit entweder der Klasse 'foo' oder 'bar' ab.Ha, das ist wirklich cool! Ich wusste nicht, dass es das kann, danke für das Teilen.