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 18:30] gkochde:modul:m291:learningunits:lu05:theorie:a_dom_traversal [2026/03/08 23:24] (aktuell) gkoch
Zeile 6: Zeile 6:
 In diesem Block bauen Sie ein interaktives FAQ Accordion. In diesem Block bauen Sie ein interaktives FAQ Accordion.
  
-<WRAP center round box 60%> +<WRAP center round box 80%> 
-{{ :de:modul:m291:learningunits:lu05:theorie:faq-toggle.gif?nolink&800 |FAQ Accordion}}+{{ :de:modul:m291:learningunits:lu05:theorie:faq-toggle.gif?nolink |FAQ Accordion}}
 </WRAP> </WRAP>
  
 Laden Sie hier das Figma-File, die Start-HTML und das Readme herunter: Laden Sie hier das Figma-File, die Start-HTML und das Readme herunter:
  
-<WRAP center round download 60%>+<WRAP center round download 80%>
 {{ :de:modul:m291:learningunits:lu04:aufgaben:faq-accordion_m291.zip | Accordion Starter}} {{ :de:modul:m291:learningunits:lu04:aufgaben:faq-accordion_m291.zip | Accordion Starter}}
 </WRAP> </WRAP>
  
-<WRAP tip round center 60%>+<WRAP tip round center 80%>
 **Projektaufbau:** Erstellen Sie drei Dateien: ''index.html'', ''script.js'' und ''style.css''. **Projektaufbau:** Erstellen Sie drei Dateien: ''index.html'', ''script.js'' und ''style.css''.
 {{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-08_at_16.20.42.png?nolink&400 |}} {{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-08_at_16.20.42.png?nolink&400 |}}
Zeile 70: Zeile 70:
 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: 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:
  
-<WRAP center round box 60%>+<WRAP center round box 80%>
 Dieser Code kommt ins ''style.css''. Dieser Code kommt ins ''style.css''.
  
Zeile 86: Zeile 86:
 Was macht dieser Code (später)? 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.): 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.):
-{{:de:modul:m291:learningunits:lu05:theorie:faq-class-toggle.gif?nolink&800 |}}+{{:de:modul:m291:learningunits:lu05:theorie:faq-class-toggle.gif?nolink |}}
 </WRAP> </WRAP>
  
 ===== 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?
-<WRAP center round box 60%>+<WRAP center round box 80%>
 <code javascript> <code javascript>
 const panel = document.querySelector('.panel'); const panel = document.querySelector('.panel');
Zeile 109: Zeile 109:
 Die Lösung: ''querySelectorAll()'' gibt **alle** passenden Elemente zurück: Die Lösung: ''querySelectorAll()'' gibt **alle** passenden Elemente zurück:
  
-<WRAP center round box 60%>+<WRAP center round box 80%>
 <code javascript> <code javascript>
 const panels  = document.querySelectorAll('.panel'); const panels  = document.querySelectorAll('.panel');
Zeile 137: Zeile 137:
   * ''.map()'', ''.filter()'', ''.reduce()''   * ''.map()'', ''.filter()'', ''.reduce()''
  
-<WRAP center round box 60%>+<WRAP center round box 80%>
 <code javascript> <code javascript>
 const buttons = document.querySelectorAll('.accordion-btn'); const buttons = document.querySelectorAll('.accordion-btn');
Zeile 154: Zeile 154:
 Auf einen **einzelnen Button** einen EventListener setzen würde so aussehen: Auf einen **einzelnen Button** einen EventListener setzen würde so aussehen:
  
-<WRAP center round box 60%>+<WRAP center round box 80%>
 <code javascript> <code javascript>
 button.addEventListener('click', () => { button.addEventListener('click', () => {
Zeile 164: Zeile 164:
 Da wir aber eine NodeList haben, iterieren wir mit ''forEach()'' über alle Elemente und setzen auf jedem einen Listener: Da wir aber eine NodeList haben, iterieren wir mit ''forEach()'' über alle Elemente und setzen auf jedem einen Listener:
  
-<WRAP center round box 60%>+<WRAP center round box 80%>
 <code javascript> <code javascript>
 buttons.forEach(b => { buttons.forEach(b => {
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|}}
  
-<WRAP center round box 60%>+==== 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%>
 <code javascript> <code javascript>
 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 60%>+
 <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 60%>+
 ^ Eigenschaft ^ Beschreibung ^ ^ Eigenschaft ^ Beschreibung ^
 | ''element.nextElementSibling'' | Nächstes Geschwister-Element | | ''element.nextElementSibling'' | Nächstes Geschwister-Element |
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 ausDrei Dinge müssen noch gelöst werden:
- +
-<WRAP center round box 60%> +
-<code javascript> +
-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'); +
-  }); +
-}); +
-</code> +
-</WRAP> +
- +
-Testen SieEin Panel öffnen schliesst nun alle anderen. +
- +
-Das ''setAttribute('aria-expanded', ...)'' erklären wir ausführlich in den folgenden Seiten.+
  
 +  - Alle anderen Panels schliessen, wenn eines geöffnet wird
 +  - Ein geöffnetes Panel beim erneuten Klick wieder schliessen (Toggle)
 +  - Sicherstellen, dass wir immer den ''<button>'' ansprechen – nicht ein Kind-Element davon
  
-===== Schritt 6: e.target vs. e.currentTarget =====+==== Problem: e.target kann das falsche Element sein ====
  
-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.+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.
  
-<WRAP center round box 60%>+<WRAP center round box 80%>
 ^ Eigenschaft ^ Beschreibung ^ ^ Eigenschaft ^ Beschreibung ^
 | ''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 |
- 
-<code javascript> 
-// 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 ✅ 
- 
-  }); 
-}); 
-</code> 
 </WRAP> </WRAP>
  
-==== Das finale Script (script.js) ====+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''.
  
-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):+==== Das finale Script ====
  
-<WRAP box round 80%>+<WRAP center round box 80%>
 <code javascript> <code javascript>
 const buttons = document.querySelectorAll('.accordion-btn'); const buttons = document.querySelectorAll('.accordion-btn');
Zeile 291: Zeile 269:
 buttons.forEach((button) => { buttons.forEach((button) => {
   button.addEventListener('click', (e) => {   button.addEventListener('click', (e) => {
-    const btn = e.currentTarget; +    const btn          = e.currentTarget;           // immer der <button> 
-    const panelElement = btn.nextElementSibling; +    const panelElement = btn.nextElementSibling;    // direkt das zugehörige Panel 
-    const panelIsOpen  = panelElement.classList.contains('open');+    const panelIsOpen  = panelElement.classList.contains('open'); // aktuellen Zustand lesen
  
     // Alle Panels schliessen     // Alle Panels schliessen
Zeile 311: 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.1772991038.txt.gz
  • Zuletzt geändert: 2026/03/08 18:30
  • von gkoch