Dies ist eine alte Version des Dokuments!


LU05a – DOM Traversal & NodeLists

In diesem Block bauen Sie ein interaktives FAQ Accordion.

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:

  1. CSS: im <head> mit <link href=„style.css“ rel=„stylesheet“>
  2. 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:

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.):

 Vergleich 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:

const panels  = document.querySelectorAll('.panel');
const buttons = document.querySelectorAll('.accordion-btn');
 
console.log(panels);  // → NodeList(4) [p.panel, p.panel, p.panel, p.panel]
console.log(buttons); // → NodeList(4) [button.accordion-btn, ...]


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>

 Eventlistener Symbolbild

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.

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.

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

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.

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 ✅
 
  });
});

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');
    }
  });
});
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
  • de/modul/m291/learningunits/lu05/theorie/a_dom_traversal.1772990548.txt.gz
  • Zuletzt geändert: 2026/03/08 18:22
  • von gkoch