Unterschiede

Hier werden die Unterschiede zwischen zwei Versionen angezeigt.

Link zu dieser Vergleichsansicht

Beide Seiten der vorigen Revision Vorhergehende Überarbeitung
Nächste Überarbeitung
Vorhergehende Überarbeitung
de:modul:m291:learningunits:lu05:theorie:a_dom_traversal [2026/03/08 21:20] gkochde:modul:m291:learningunits:lu05:theorie:a_dom_traversal [2026/03/08 23:24] (aktuell) gkoch
Zeile 91: Zeile 91:
 ===== Schritt 2: querySelector vs. querySelectorAll ===== ===== Schritt 2: querySelector vs. querySelectorAll =====
  
-{{:de:modul:m291:learningunits:lu05:theorie:queryselector_all_0.4x.png?direct&1100| Vergleich querySelector() vs. querySelectorAll()}}+{{:de:modul:m291:learningunits:lu05:theorie:queryselector_all_0.4x.png?direct&1200| Vergleich querySelector() vs. querySelectorAll()}}
  
 Als Erstes selektieren wir die Elemente im DOM. Was passiert, wenn wir ''querySelector()'' verwenden? Als Erstes selektieren wir die Elemente im DOM. Was passiert, wenn wir ''querySelector()'' verwenden?
Zeile 177: Zeile 177:
  
  
-===== Schritt 4: DOM Traversal – nextSibling vs. nextElementSibling =====+===== Schritt 4: Das richtige Panel finden – DOM Traversal =====
  
 {{:de:modul:m291:learningunits:lu05:theorie:siblings.jpg?direct&600|}} {{:de:modul:m291:learningunits:lu05:theorie:siblings.jpg?direct&600|}}
  
-Wenn ein Button geklickt wird, müssen wir zum benachbarten ''<p class="panel">'' navigierenDiese Technik nennt man **DOM Traversal** – das gezielte Durchqueren des DOM-Baums.+Wir haben jetzt EventListener auf allen Buttons. Wenn ein Button geklickt wird, müssen wir das zugehörige ''<p class="panel">'' öffnenDie Frage ist: **Wie wissen wir, welches Panel zu welchem Button gehört?** –> Button und Panel sind Geschwister im HTML:
  
-Der naheliegende erste Versuch mit ''nextSibling'' schlägt fehl:+{{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-08_at_23.15.46.png?direct&600|}} 
 + 
 +==== DOM Traversal mit nextElementSibling ==== 
 + 
 +Die sauberere Lösung nutzt die **Struktur des DOMs** selbst. Im HTML ist jedes Panel das direkte Geschwister-Element des zugehörigen Buttons: 
 +<WRAP center round box 80%> 
 +<code html> 
 +<div class="accordion-item"> 
 +  <button class="accordion-btn">...</button>   ← Button 
 +  <p class="panel">...</p>                     ← direkt daneben: das Panel 
 +</div> 
 +</code> 
 +</WRAP> 
 + 
 +Mit ''nextElementSibling'' navigieren wir direkt vom geklickten Button zu seinem Panel:
  
 <WRAP center round box 80%> <WRAP center round box 80%>
Zeile 189: Zeile 203:
 buttons.forEach((b) => { buttons.forEach((b) => {
   b.addEventListener('click', (e) => {   b.addEventListener('click', (e) => {
-    const panelElement = e.target.nextSibling+    const panelElement = e.target.nextElementSibling// ✅ direkt das zugehörige Panel 
-    panelElement.classList.add('open'); // ❌ Fehler in der Console! +    panelElement.classList.add('open');
-    console.log(e.target.nextSibling);  // → #text  (ein Zeilenumbruch!)+
   });   });
 }); });
Zeile 197: Zeile 210:
 </WRAP> </WRAP>
  
-**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.+==== Warum nicht einfach nextSibling==== 
 + 
 +Der erste Versuch mit ''nextSibling'' scheitert:
  
-Die richtige Methode: ''nextElementSibling'' überspringt Text-Nodes und gibt das nächste **HTML-Element** zurück: 
 <WRAP center round box 80%> <WRAP center round box 80%>
 <code javascript> <code javascript>
 buttons.forEach((b) => { buttons.forEach((b) => {
   b.addEventListener('click', (e) => {   b.addEventListener('click', (e) => {
-    const panelElement = e.target.nextElementSibling// ✅ korrekt +    const panelElement = e.target.nextSibling
-    panelElement.classList.add('open');+    panelElement.classList.add('open'); // ❌ Fehler in der Console! 
 +    console.log(e.target.nextSibling);  // → #text  (ein Zeilenumbruch!)
   });   });
 }); });
Zeile 211: Zeile 226:
 </WRAP> </WRAP>
  
-Testen Sie: Alle Panels sollten sich jetzt öffnen lassen – aber noch nicht schliessen. +**Warum?** 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 der Zeilenumbruch nach ''<button>'', kein HTML-Element''nextElementSibling'' überspringt Text-Nodes und gibt immer das nächste **HTML-Element** zurück.
- +
-==== Übersicht: Traversal-Eigenschaften ====+
  
 <WRAP center round box 80%> <WRAP center round box 80%>
Zeile 224: Zeile 237:
 </WRAP> </WRAP>
  
-===== Schritt 5: Alle anderen Panels schliessen =====+Testen Sie: Alle Panels sollten sich jetzt öffnen lassen – aber noch nicht schliessen. 
 + 
 +===== Schritt 5 & 6Zum finalen Script =====
  
 {{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-08_at_18.28.50.png?direct&600| Ein einziges Panel soll offen sein.}} {{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-08_at_18.28.50.png?direct&600| Ein einziges Panel soll offen sein.}}
  
-Beim klassischen Accordion soll immer nur ein Panel offen sein. Vor dem Öffnen des geklickten Panels schliessen wir deshalb alle:+Jetzt bauen wir den Code Schritt für Schritt zum finalen Script aus. Drei Dinge müssen noch gelöst werden:
  
-<WRAP center round box 80%> +  - Alle anderen Panels schliessen, wenn eines geöffnet wird 
-<code javascript> +  - Ein geöffnetes Panel beim erneuten Klick wieder schliessen (Toggle
-buttons.forEach((b=> { +  - Sicherstellen, dass wir immer den ''<button>'' ansprechen – nicht ein Kind-Element davon
-  b.addEventListener('click', (e) =+
-    const panelElement = e.target.nextElementSibling;+
  
-    // Alle Panels schliessen +==== Problem: e.target kann das falsche Element sein ====
-    buttons.forEach((andererBtn) => { +
-      andererBtn.nextElementSibling.classList.remove('open'); +
-      andererBtn.setAttribute('aria-expanded', 'false'); +
-    });+
  
-    // Geklicktes Panel öffnen +Wenn der Button ein Icon enthält (bei uns: das ''+'' und ''−'' via ''::after''), kann ein Klick genau auf dieses Icon landen. Dann zeigt ''e.target'' auf das Icon – nicht auf den Button selbst.
-    panelElement.classList.add('open'); +
-  }); +
-}); +
-</code> +
-</WRAP> +
- +
-Testen Sie: Ein Panel öffnen schliesst nun alle anderen. +
- +
-Das ''setAttribute('aria-expanded', ...)'' erklären wir ausführlich in den folgenden Seiten. +
- +
- +
-===== Schritt 6e.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.+
  
 <WRAP center round box 80%> <WRAP center round box 80%>
Zeile 262: Zeile 257:
 | ''event.target'' | Das Element, das das Event **ursprünglich ausgelöst** hat – kann ein Kind-Element (wie ein Icon) sein | | ''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 | | ''event.currentTarget'' | Das Element, an dem der **EventListener registriert** wurde – immer der Button |
 +</WRAP>
 +
 +Die Lösung: Wir lesen den Button immer via ''e.currentTarget'' aus und speichern ihn in einer Variable. Damit haben wir auch die stabile Basis für ''nextElementSibling''.
 +
 +==== Das finale Script ====
  
 +<WRAP center round box 80%>
 <code javascript> <code javascript>
-// Im finalen Projekt: Button enthält ein SVG-Icon via CSS ::after +const buttons document.querySelectorAll('.accordion-btn');
-// <button class="accordion-btn">Frage...</button>  +  .accordion-btn::after { content: url(...}+
  
-buttons.forEach((b) => { +buttons.forEach((button) => { 
-  b.addEventListener('click', (e) => {+  button.addEventListener('click', (e) => { 
 +    const btn          = e.currentTarget;           // immer der <button> 
 +    const panelElement = btn.nextElementSibling;    // direkt das zugehörige Panel 
 +    const panelIsOpen  = panelElement.classList.contains('open'); // aktuellen Zustand lesen
  
-    console.log(e.target); +    // Alle Panels schliessen 
-    // → Kann das ::after-Pseudo-Element oder ein inneres Element sein +    buttons.forEach((andererBtn) => { 
- +      andererBtn.nextElementSibling.classList.remove('open'); 
-    console.log(e.currentTarget); +      andererBtn.setAttribute('aria-expanded', 'false'); 
-    // → Immer der <button> – weil dort addEventListener(registriert wurde ✅+    });
  
 +    // Dieses Panel öffnen – aber nur wenn es vorher geschlossen war
 +    if (!panelIsOpen) {
 +      panelElement.classList.add('open');
 +      btn.setAttribute('aria-expanded', 'true');
 +    }
   });   });
 }); });
Zeile 281: Zeile 289:
 </WRAP> </WRAP>
  
 +**Was passiert bei jedem Klick?**
  
 +  - ''e.currentTarget'' gibt uns sicher den ''<button>'', egal ob auf Text oder Icon geklickt wurde
 +  - ''btn.nextElementSibling'' gibt das direkt zugehörige Panel zurück
 +  - ''classList.contains('open')'' liest den aktuellen Zustand, bevor wir alles schliessen
 +  - Das innere ''forEach()'' schliesst alle Panels und setzt ''aria-expanded'' auf ''false''
 +  - Die ''if''-Bedingung öffnet das geklickte Panel nur dann, wenn es vorher zu war – so funktioniert auch der Toggle (zweimal klicken schliesst)
 +
 +Das ''setAttribute('aria-expanded', ...)'' erklären wir ausführlich in den folgenden Seiten.
  
  
  • de/modul/m291/learningunits/lu05/theorie/a_dom_traversal.1773001211.txt.gz
  • Zuletzt geändert: 2026/03/08 21:20
  • von gkoch