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 17:44] 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 123: Zeile 123:
 </WRAP> </WRAP>
  
- 
- 
-^ Methode ^ Gibt zurück ^ Einsatz ^ 
-| ''querySelector('.panel')'' | Erstes passendes Element (oder ''null'') | Wenn genau ein Element gemeint ist | 
-| ''querySelectorAll('.panel')'' | Alle passenden Elemente als **NodeList** | Wenn mehrere Elemente angesprochen werden | 
  
  
Zeile 135: Zeile 130:
  
 Was eine NodeList **kann:** Was eine NodeList **kann:**
-  * Per Index zugreifen: ''list[0]'', ''list[1]'' ... +  * Per Index auf eine einzelnes Element zugreifen: ''list[0]'', ''list[1]'' ... 
-  * Länge auslesen: ''list.length''+  * Wieviele Elemente hat es davon?: ''list.length''
   * ''forEach()'' – über alle Elemente iterieren   * ''forEach()'' – über alle Elemente iterieren
  
Zeile 142: Zeile 137:
   * ''.map()'', ''.filter()'', ''.reduce()''   * ''.map()'', ''.filter()'', ''.reduce()''
  
 +<WRAP center round box 80%>
 <code javascript> <code javascript>
 const buttons = document.querySelectorAll('.accordion-btn'); const buttons = document.querySelectorAll('.accordion-btn');
Zeile 149: Zeile 145:
  
 </code> </code>
 +</WRAP>
  
  
 ===== Schritt 3: forEach() – EventListener auf alle Buttons setzen ===== ===== Schritt 3: forEach() – EventListener auf alle Buttons setzen =====
  
-Auf einen einzelnen Button einen EventListener setzen würde so aussehen:+{{:de:modul:m291:learningunits:lu05:theorie:eventlistner_0.5x.png?direct&500| Eventlistener Symbolbild}}
  
 +Auf einen **einzelnen Button** einen EventListener setzen würde so aussehen:
 +
 +<WRAP center round box 80%>
 <code javascript> <code javascript>
 button.addEventListener('click', () => { button.addEventListener('click', () => {
Zeile 160: Zeile 160:
 }); });
 </code> </code>
 +</WRAP>
  
 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 80%>
 <code javascript> <code javascript>
 buttons.forEach(b => { buttons.forEach(b => {
Zeile 170: Zeile 172:
 }); });
 </code> </code>
 +</WRAP>
  
 Öffnen Sie die DevTools-Console und klicken Sie auf verschiedene Buttons. ''e.target'' sollte immer den jeweiligen geklickten Button ausgeben. Ö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 =====+===== Schritt 4: Das richtige Panel finden – DOM Traversal =====
  
-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.+{{:de:modul:m291:learningunits:lu05:theorie:siblings.jpg?direct&600|}}
  
-Der naheliegende erste Versuch mit ''nextSibling'' schlägt fehl:+Wir haben jetzt EventListener auf allen Buttons. Wenn ein Button geklickt wird, müssen wir das zugehörige ''<p class="panel">'' öffnen. Die Frage ist: **Wie wissen wir, welches Panel zu welchem Button gehört?** –> Button und Panel sind Geschwister im HTML:
  
 +{{: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%>
 <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!)+
   });   });
 }); });
 </code> </code>
 +</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? ====
  
-Die richtige Methode: ''nextElementSibling'' überspringt Text-Nodes und gibt das nächste **HTML-Element** zurück:+Der erste Versuch mit ''nextSibling'' scheitert:
  
 +<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!)
   });   });
 }); });
 </code> </code>
 +</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%>
 ^ Eigenschaft ^ Beschreibung ^ ^ Eigenschaft ^ Beschreibung ^
 | ''element.nextElementSibling'' | Nächstes Geschwister-Element | | ''element.nextElementSibling'' | Nächstes Geschwister-Element |
Zeile 213: Zeile 235:
 | ''element.firstElementChild'' | Erstes Kind-Element | | ''element.firstElementChild'' | Erstes Kind-Element |
 | ''element.lastElementChild'' | Letztes Kind-Element | | ''element.lastElementChild'' | Letztes Kind-Element |
 +</WRAP>
  
-===== Schritt 5: Alle anderen Panels schliessen =====+Testen Sie: Alle Panels sollten sich jetzt öffnen lassen – aber noch nicht schliessen.
  
-Beim klassischen Accordion soll immer nur ein Panel offen sein. Vor dem Öffnen des geklickten Panels schliessen wir deshalb alle:+===== Schritt 5 & 6Zum finalen Script =====
  
-<code javascript> +{{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-08_at_18.28.50.png?direct&600| Ein einziges Panel soll offen sein.}}
-buttons.forEach((b) => { +
-  b.addEventListener('click', (e) => { +
-    const panelElement = e.target.nextElementSibling;+
  
-    // Alle Panels schliessen +Jetzt bauen wir den Code Schritt für Schritt zum finalen Script ausDrei Dinge müssen noch gelöst werden:
-    buttons.forEach((andererBtn) => { +
-      andererBtn.nextElementSibling.classList.remove('open'); +
-      andererBtn.setAttribute('aria-expanded', 'false'); +
-    });+
  
-    // Geklicktes Panel öffnen +  - Alle anderen Panels schliessen, wenn eines geöffnet wird 
-    panelElement.classList.add('open'); +  - Ein geöffnetes Panel beim erneuten Klick wieder schliessen (Toggle
-  }); +  - Sicherstellen, dass wir immer den ''<button>'' ansprechen – nicht ein Kind-Element davon
-}); +
-</code>+
  
-Testen SieEin Panel öffnen schliesst nun alle anderen.+==== Probleme.target kann das falsche Element sein ====
  
-Das ''setAttribute('aria-expanded', ...)'' erkläre wir ausführlich in [[lu05c_accessibility|LU05c]]. +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.
- +
- +
-===== 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%>
 ^ Eigenschaft ^ Beschreibung ^ ^ Eigenschaft ^ Beschreibung ^
-| ''event.target'' | Das Element, das das Event **ursprünglich ausgelöst** hat – kann ein Kind-Element 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>
  
-<code javascript> +Die LösungWir lesen den Button immer via ''e.currentTarget'' aus und speichern ihn in einer VariableDamit haben wir auch die stabile Basis für ''nextElementSibling''.
-// 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> +
- +
-==== 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):+==== Das finale Script ====
  
 +<WRAP center round box 80%>
 <code javascript> <code javascript>
 const buttons = document.querySelectorAll('.accordion-btn'); const buttons = document.querySelectorAll('.accordion-btn');
Zeile 274: Zeile 269:
 buttons.forEach((button) => { buttons.forEach((button) => {
   button.addEventListener('click', (e) => {   button.addEventListener('click', (e) => {
-    const btn          = e.currentTarget;         // ✅ immer der <button> +    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 292: Zeile 287:
 }); });
 </code> </code>
- 
-<WRAP important> 
-**Faustregel:** Verwenden Sie ''e.currentTarget'' statt ''e.target'', wenn Sie sicherstellen müssen, dass Sie das Element ansprechen, auf dem der EventListener registriert wurde – und nicht ein mögliches Kind-Element davon. 
 </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.1772988275.txt.gz
  • Zuletzt geändert: 2026/03/08 17:44
  • von gkoch