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:42] 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 60: Zeile 60:
  
 \\ \\
-{{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-05_at_11.48.01.png?nolink&400|}}+Anschliessend sollte es so aussehen im Browser: 
 +{{:de:modul:m291:learningunits:lu05:theorie:screenshot_2026-03-05_at_11.48.01.png?nolink&600|}}
 </WRAP> </WRAP>
  
Zeile 69: 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 84: Zeile 85:
 </code> </code>
 Was macht dieser Code (später)? Was macht dieser Code (später)?
-Er blendet nicht aktive Antworten aus und blended das aktive Element ein: +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&900| 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 108: 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 122: 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 134: 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 141: 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 148: 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 159: 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 169: 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 212: 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 273: 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 291: 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.1772988122.txt.gz
  • Zuletzt geändert: 2026/03/08 17:42
  • von gkoch