Abschnitt 10 · Lektion 3
Durch den Baum laufen
Ein Element gefunden zu haben ist selten das Ziel. Meistens willst du von dort aus weiter: zu seiner Karte, zu seiner Überschrift, zum nächsten Eintrag. Dafür kennt jedes Element seine Nachbarschaft.
Drei Richtungen
<div class="liste">
<article id="karte-1"><h2>Erster Kurs</h2><button>Mehr</button></article>
<article id="karte-2"><h2>Zweiter Kurs</h2><button>Mehr</button></article>
<article id="karte-3"><h2>Dritter Kurs</h2><button>Mehr</button></article>
</div> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
h2 {
font-size: 1.2rem;
margin: 1.25rem 0 0.5rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
} const zweite = document.querySelector("#karte-2");
// Nach oben.
console.log("Eltern:", zweite.parentElement.className);
// Nach unten.
console.log("Kinder:", zweite.children.length);
console.log("erstes Kind:", zweite.firstElementChild.tagName);
console.log("letztes Kind:", zweite.lastElementChild.tagName);
// Zur Seite.
console.log("davor:", zweite.previousElementSibling.id);
console.log("danach:", zweite.nextElementSibling.id);
// Am Rand gibt es keinen Nachbarn. Dann kommt null.
const erste = document.querySelector("#karte-1");
console.log("vor der ersten:", erste.previousElementSibling); Nach oben geht es mit parentElement. Nach unten mit children, firstElementChild und lastElementChild. Zur Seite mit nextElementSibling und previousElementSibling.
Die Namen sind lang und dafür eindeutig. children ist eine Liste, firstElementChild ein einzelnes Element, und alle Geschwister-Eigenschaften liefern genau eines.
Am Rand gibt es keinen Nachbarn, und dann kommt null. Vor dem ersten Kind steht kein previousElementSibling, über <html> steht kein parentElement. Das ist der Fall, um den sich Lektion 10.4 kümmert.
Warum immer die lange Variante
<div class="liste">
<article>Erster</article>
<article>Zweiter</article>
</div> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
} const liste = document.querySelector(".liste");
// Mit Element im Namen: nur Elemente.
console.log("children:", liste.children.length);
console.log("firstElementChild:", liste.firstElementChild.tagName);
// Ohne Element im Namen: auch Textknoten. Und zwischen
// den Tags im Markup steht ein Zeilenumbruch mit Einzug,
// also ein Textknoten.
console.log("childNodes:", liste.childNodes.length);
console.log("firstChild:", liste.firstChild.nodeName);
// Deshalb ist firstChild fast nie das, was man meint.
// JSON.stringify setzt den Text in Anführungszeichen und schreibt
// den Umbruch als \n, sonst sähe der Leerraum in der Console
// wie nichts aus. Was JSON sonst ist, kommt in Abschnitt 16.
console.log("firstChild Inhalt:", JSON.stringify(liste.firstChild.textContent));
// Und ohne Leerraum im Markup wäre es plötzlich richtig,
// was die Sache noch verwirrender macht.
const eng = document.createElement("div");
eng.innerHTML = "<b>a</b><i>b</i>";
console.log("ohne Leerraum:", eng.firstChild.nodeName); Zu fast jeder dieser Eigenschaften gibt es eine zweite ohne das Wort Element: childNodes, firstChild, nextSibling. Die sind älter, und sie zählen Knoten statt Elemente.
Ein Knoten ist auch ein Textstück. Und zwischen zwei Tags im Markup steht fast immer ein Zeilenumbruch mit Einzug, also Text. Das ist ein Textknoten, auch wenn er nichts Sichtbares enthält.
Daraus folgt die Überraschung: liste.childNodes.length ist bei zwei Artikeln nicht 2, sondern 5, und liste.firstChild ist nicht der erste Artikel, sondern der Zeilenumbruch davor.
Zähl selbst nach: tipp document.querySelector(".liste").childNodes.length in die Eingabezeile der Console, dort steht 5. Schieb danach die drei Zeilen im Markup zu einer einzigen zusammen und frag noch einmal. Jetzt sind es 2, und dein Code hätte plötzlich recht, ohne dass du ihn angefasst hast.
Besonders unangenehm ist, dass es manchmal doch funktioniert. Steht das Markup ohne Leerraum in einer Zeile, ist firstChild plötzlich das erwartete Element. Wer sich darauf verlässt, hat eine Zeitbombe: sobald jemand den Code formatiert, geht es kaputt.
Die Regel ist deshalb einfach: nimm immer die Variante mit Element im Namen. Es gibt in diesem Kurs keinen Fall, in dem die andere richtig wäre.
Der wichtigste von allen
<div class="liste">
<article id="karte-2" class="karte">
<h2>Zweiter Kurs</h2>
<p>Ein Absatz <button>Mehr</button></p>
</article>
</div> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
h2 {
font-size: 1.2rem;
margin: 1.25rem 0 0.5rem;
}
p {
margin: 0 0 0.75rem;
}
button,
input,
select,
textarea {
font: inherit;
}
button {
padding: 0.4rem 0.8rem;
border: 1px solid #d6d3d1;
border-radius: 6px;
background: #fafaf9;
cursor: pointer;
} const knopf = document.querySelector("button");
// Von Hand hangeln, wenn man die Struktur genau kennt.
console.log("von Hand:", knopf.parentElement.parentElement.id);
// Mit closest, wenn man sie nicht genau kennt. Es sucht vom
// Element aus nach oben den nächsten passenden Vorfahren.
console.log("mit closest:", knopf.closest(".karte").id);
// Das Element selbst zählt mit. closest fängt bei ihm an.
console.log("bei sich selbst:", knopf.closest("button").tagName);
// Kein passender Vorfahre heißt null.
console.log("nichts gefunden:", knopf.closest(".gibtesnicht"));
// Und von der Karte aus wieder nach unten suchen.
const karte = knopf.closest(".karte");
console.log("Überschrift dazu:", karte.querySelector("h2").textContent); closest sucht vom Element aus nach oben den nächsten Vorfahren, der zu einem Selektor passt. Das Element selbst zählt dabei mit.
Warum das der wichtigste ist, sieht man am Beispiel. Von Hand geht es auch: knopf.parentElement.parentElement. Aber diese Kette schreibt die Struktur des Markups in den Code. Kommt später ein <div> dazwischen, stimmt sie nicht mehr, und zwar still.
knopf.closest(".karte") sagt dagegen, was gemeint ist: die Karte, in der dieser Knopf steckt. Wie viele Ebenen dazwischenliegen, ist der Funktion egal und dem Leser auch.
Probier es am Beispiel mit dem Inspector aus Lektion 10.2: Klick den Knopf fest, dann ist er in der Eingabezeile der Console $0. $0.closest(".karte") antwortet <article id="karte-2" class="karte">, $0.parentElement dagegen nur mit <p>, dem Absatz dazwischen. Und $0.closest(".gibtesnicht") antwortet null, ohne Fehler: Kein passender Vorfahre ist eine Antwort, kein Absturz.
Genau das braucht die Event-Delegation in Lektion 12.6. Dort bekommst du das geklickte Element und musst herausfinden, zu welchem Eintrag es gehört. Ohne closest schreibt man dafür Kletterei von Hand, mit closest eine Zeile.
Kein passender Vorfahre heißt wieder null. Und der übliche Doppelschritt ist: mit closest nach oben zum Container, von dort mit querySelector wieder nach unten zum gesuchten Teil.
Passt dieses Element?
<div class="karte hervor" data-kurs="js"><h2>Zweiter Kurs</h2></div>
<div class="karte"><h2>Dritter Kurs</h2></div> body {
font-family: system-ui, sans-serif;
max-width: 40rem;
margin: 0;
padding: 1rem;
line-height: 1.6;
color: #1c1917;
}
h2 {
font-size: 1.2rem;
margin: 1.25rem 0 0.5rem;
} const erste = document.querySelector(".karte");
// matches prüft, ob ein Element zu einem Selektor passt.
console.log("hervor:", erste.matches(".hervor"));
console.log("mit Attribut:", erste.matches("[data-kurs]"));
console.log("nicht:", erste.matches(".gibtesnicht"));
// Damit lässt sich eine NodeList filtern, ohne den
// Selektor zweimal zu schreiben.
const alle = Array.from(document.querySelectorAll(".karte"));
const hervorgehoben = alle.filter((karte) => karte.matches(".hervor"));
console.log("gefiltert:", hervorgehoben.length);
// classList.contains kann fast dasselbe, aber nur für Klassen.
console.log("classList:", erste.classList.contains("hervor"));
// matches versteht dagegen jeden Selektor.
console.log("zusammengesetzt:", erste.matches(".karte[data-kurs='js']")); matches beantwortet die Frage, ob ein Element zu einem Selektor passt. Es liefert true oder false und ändert nichts.
Damit lässt sich eine Liste von Elementen filtern, ohne den Selektor doppelt zu schreiben, und in Abschnitt 12 lässt sich prüfen, ob das geklickte Element überhaupt interessant ist.
classList.contains kann fast dasselbe, allerdings nur für Klassen. matches versteht jeden Selektor, also auch Attribute und Kombinationen. Beide sind richtig, contains liest sich bei einer reinen Klassenfrage etwas direkter.
Damit ist der Werkzeugkasten zum Lesen fast vollständig. Was noch fehlt, ist der Fall, dass nichts gefunden wird, und der ist wichtig genug für eine eigene Lektion.
Zum Mitnehmen
Nimm immer die Variante mit Element im Namen. Die ohne zählt Textknoten mit, und ein Zeilenumbruch im Markup ist ein Textknoten.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe im Editor, direkt im Browser geprüft
Öffnet sich mit dem Basis Konto.