Dies ist eine alte Version des Dokuments!
LU05a – DOM Traversal & NodeLists
Ausgangslage: Das Accordion-Projekt
In diesem Block bauen Sie ein interaktives FAQ Accordion.
Laden Sie hier das Figma-File, die Start-HTML und das Readme herunter:
Projektaufbau: Erstellen Sie drei Dateien: index.html, script.js und style.css.
Verlinken Sie CSS- und JavaScript-File im HTML:
- CSS: im
<head>mit<link href=„style.css“ rel=„stylesheet“> - JS-Script: am Ende des
<body>mit<script src=„script.js“></script>.
Testen Sie zuerst, ob die Verlinkung klappt – dieser Code gehört in script.js:
console.log('Hello World!'); // erscheint das in der DevTools-Console?
Der Aufbau beginnt bewusst ohne Styling – Funktionalität zuerst. Das HTML-Grundgerüst sieht so aus:
<div class="accordion"> <h1>FAQs</h1> <div class="accordion-item"> <button class="accordion-btn" aria-expanded="false"> What is this project, and how will it help me? </button> <p class="panel"> It's a small but mighty mission: you'll build an FAQ accordion... </p> </div> <div class="accordion-item"> <button class="accordion-btn" aria-expanded="false"> Is this free? </button> <p class="panel"> Yes. No coins, no secret handshake... </p> </div> <!-- weitere .accordion-item ... --> </div> <script src="script.js"></script>
Anschliessend sollte es so aussehen im Browser:
Schritt 1: Panels (Antworten) mit CSS ein- und ausblenden
Bevor wir JavaScript schreiben, definieren wir die zwei möglichen Zustände im CSS. Die Klasse open wird später per JS gesetzt oder entfernt – das CSS übernimmt das An- und Ausblenden:
Dieser Code kommt ins style.css.
/* Standardmässig werden die Antworten versteckt */ .panel { display: none; } /* Sichtbar werden sie nur, wenn Klasse 'open' gesetzt */ .panel.open { display: block; }
Was macht dieser Code (später)?
Er blendet nicht aktive Antworten aus und blended das aktive Element ein (div mit Klasse .panel bekommt eine zweite Klasse .open sobald es geklickt wird.):
Schritt 2: querySelector vs. querySelectorAll
Als Erstes selektieren wir die Elemente im DOM. Was passiert, wenn wir querySelector() verwenden?
const panel = document.querySelector('.panel'); const btn = document.querySelector('.accordion-btn'); console.log(panel); // → <p class="panel">...</p> NUR das erste! console.log(btn); // → <button class="accordion-btn">...</button> NUR das erste!
querySelector() gibt immer nur das erste passende Element zurück. Für ein Accordion mit vier Fragen reicht das nicht – wir brauchen alle.
Die Lösung: querySelectorAll() gibt alle passenden Elemente zurück:
Was ist eine NodeList?
querySelectorAll() gibt kein gewöhnliches Array zurück, sondern eine NodeList – eine listenartige Sammlung von DOM-Elementen.
Was eine NodeList kann:
- Per Index auf eine einzelnes Element zugreifen:
list[0],list[1]… - Wieviele Elemente hat es davon?:
list.length forEach()– über alle Elemente iterieren
Was eine NodeList standardmässig nicht kann (anders als ein echtes Array):
.map(),.filter(),.reduce()
const buttons = document.querySelectorAll('.accordion-btn'); console.log(buttons.length); // → 4 console.log(buttons[0]); // → <button class="accordion-btn">...</button>
Schritt 3: forEach() – EventListener auf alle Buttons setzen
Auf einen einzelnen Button einen EventListener setzen würde so aussehen:
button.addEventListener('click', () => { console.log('Geklickt!'); });
Da wir aber eine NodeList haben, iterieren wir mit forEach() über alle Elemente und setzen auf jedem einen Listener:
buttons.forEach(b => { b.addEventListener('click', (e) => { console.log('Geklickt:', e.target); }); });
Öffnen Sie die DevTools-Console und klicken Sie auf verschiedene Buttons. e.target sollte immer den jeweiligen geklickten Button ausgeben.
Schritt 4: DOM Traversal – nextSibling vs. nextElementSibling
Wenn ein Button geklickt wird, müssen wir zum benachbarten <p class=„panel“> navigieren. Diese Technik nennt man DOM Traversal – das gezielte Durchqueren des DOM-Baums.
Der naheliegende erste Versuch mit nextSibling schlägt fehl:
buttons.forEach((b) => { b.addEventListener('click', (e) => { const panelElement = e.target.nextSibling; panelElement.classList.add('open'); // ❌ Fehler in der Console! console.log(e.target.nextSibling); // → #text (ein Zeilenumbruch!) }); });
Warum klappt das nicht? Im HTML-Quellcode sind Zeilenumbrüche und Leerzeichen zwischen Tags eigenständige DOM-Nodes (sog. Text-Nodes). nextSibling gibt den allernächsten Node zurück – das ist in unserem Fall der Zeilenumbruch nach <button>, kein HTML-Element.
Die richtige Methode: nextElementSibling überspringt Text-Nodes und gibt das nächste HTML-Element zurück:
buttons.forEach((b) => { b.addEventListener('click', (e) => { const panelElement = e.target.nextElementSibling; // ✅ korrekt panelElement.classList.add('open'); }); });
Testen Sie: Alle Panels sollten sich jetzt öffnen lassen – aber noch nicht schliessen.
Übersicht: Traversal-Eigenschaften
| Eigenschaft | Beschreibung |
|---|---|
element.nextElementSibling | Nächstes Geschwister-Element |
element.previousElementSibling | Vorheriges Geschwister-Element |
element.parentElement | Elternelement |
element.firstElementChild | Erstes Kind-Element |
element.lastElementChild | Letztes Kind-Element |
Schritt 5: Alle anderen Panels schliessen
Beim klassischen Accordion soll immer nur ein Panel offen sein. Vor dem Öffnen des geklickten Panels schliessen wir deshalb alle:
buttons.forEach((b) => { b.addEventListener('click', (e) => { const panelElement = e.target.nextElementSibling; // Alle Panels schliessen buttons.forEach((andererBtn) => { andererBtn.nextElementSibling.classList.remove('open'); andererBtn.setAttribute('aria-expanded', 'false'); }); // Geklicktes Panel öffnen panelElement.classList.add('open'); }); });
Testen Sie: Ein Panel öffnen schliesst nun alle anderen.
Das setAttribute('aria-expanded', …) erklären wir ausführlich in den folgenden Seiten.
Schritt 6: e.target vs. e.currentTarget
Unser Code funktioniert – hat aber noch eine versteckte Schwachstelle. Sobald wir Icons innerhalb des Buttons hinzufügen (z.B. als Pseudo-Elemente via CSS, was wir beim Styling noch tun), kann e.target auf das Icon zeigen, nicht auf den Button selbst.
| Eigenschaft | Beschreibung |
|---|---|
event.target | Das Element, das das Event ursprünglich ausgelöst hat – kann ein Kind-Element (wie ein Icon) sein |
event.currentTarget | Das Element, an dem der EventListener registriert wurde – immer der Button |
// Im finalen Projekt: Button enthält ein SVG-Icon via CSS ::after // <button class="accordion-btn">Frage...</button> + .accordion-btn::after { content: url(...) } buttons.forEach((b) => { b.addEventListener('click', (e) => { console.log(e.target); // → Kann das ::after-Pseudo-Element oder ein inneres Element sein console.log(e.currentTarget); // → Immer der <button> – weil dort addEventListener() registriert wurde ✅ }); });
Das finale Script (script.js)
Im abgeschlossenen Accordion-Projekt sieht das vollständige Script so aus. Beachten Sie die Verwendung von e.currentTarget und die zusätzliche Toggle-Logik (Klick auf ein offenes Panel schliesst es):
const buttons = document.querySelectorAll('.accordion-btn'); buttons.forEach((button) => { button.addEventListener('click', (e) => { const btn = e.currentTarget; const panelElement = btn.nextElementSibling; const panelIsOpen = panelElement.classList.contains('open'); // Alle Panels schliessen buttons.forEach((andererBtn) => { andererBtn.nextElementSibling.classList.remove('open'); andererBtn.setAttribute('aria-expanded', 'false'); }); // Dieses Panel öffnen – aber nur wenn es vorher geschlossen war if (!panelIsOpen) { panelElement.classList.add('open'); btn.setAttribute('aria-expanded', 'true'); } }); });
Zusammenfassung
| Konzept | Merksatz |
|---|---|
querySelector() | Gibt ein Element zurück – das erste passende |
querySelectorAll() | Gibt eine NodeList zurück – alle passenden |
forEach() | Iteriert über eine NodeList wie über ein Array |
nextElementSibling | Nächstes Geschwister-Element (überspringt Text-Nodes) |
e.target | Das Element, das das Event ausgelöst hat (kann Kind-Element sein) |
e.currentTarget | Das Element mit dem registrierten EventListener |




