Zum Inhalt
wannacode

Ereignisse

Kurz gesagt: Ereignisse melden JavaScript, dass jemand geklickt oder getippt hat: addEventListener hört zu, die meisten Ereignisse steigen im Baum auf, und bei der Delegation bedient ein Listener viele Elemente.

Problem

Das neue Produkt wird nicht hinzugefügt

Der Katalog zeigt zwei Produkte. „Mehr anzeigen“ fügt ein drittes hinzu. „Hinzufügen“ bei den ersten beiden erhöht den Warenkorb, aber „Hinzufügen“ beim neuen Produkt tut nichts. In der Konsole steht kein Fehler.

HTML, CSS, JS

Klick auf „Hinzufügen“ beim Brot, dann auf „Mehr anzeigen“, dann auf „Hinzufügen“ beim Käse. Der Warenkorb bleibt bei 1. Der Käse-Button sieht aus wie die anderen und hat denselben Text. Was fehlt ihm?

Die Idee

Ereignis und Listener

Wenn die Käuferin klickt, eine Taste drückt oder in ein Feld tippt, erzeugt der Browser ein Ereignis (Event): ein Objekt, das sagt, was wo passiert ist. Der Code wartet mit einem Listener darauf:

const button = document.querySelector('#plus');
button.addEventListener('click', (event) => {
console.log(event.type, event.target);
});
  • Das erste Argument ist die Art des Ereignisses: click, keydown, input, submit.
  • Das zweite ist eine Funktion. Der Browser ruft sie bei jedem Ereignis auf und übergibt ihr das Event-Objekt. Pfeilfunktionen kommen in der Lektion über Lambdas im JavaScript-Kurs dran.
  • Du übergibst die Funktion, du rufst sie nicht auf: addEventListener('click', save), ohne Klammern. save() würde sie sofort aufrufen.

Das Event-Objekt hat Felder:

  • event.target ist das Element, an dem das Ereignis passiert ist: der Button, auf den geklickt wurde.
  • event.currentTarget ist das Element, dessen Listener gerade läuft.
  • event.key ist bei keydown die Taste: 'Enter', 'Escape', 'a'.

Jetzt ist klar, was im Problem fehlt. Die Listener werden einmal beim Laden der Seite an die Buttons gehängt, die in diesem Moment existieren. Der Käse-Button entsteht später und hat keinen Listener.

Probier es aus

Plus und Minus

Baue die Mengen-Buttons: „+“ erhöht die Menge um 1, „−“ verringert sie um 1, aber nicht unter 1. Die Menge steht in #qty.

HTML, CSS, JS

Die Idee

Das Ereignis steigt auf

Der Button steckt in <li>, <li> steckt in <ul>, und <ul> steckt in <main>. Ein Klick auf den Button ist auch ein Klick auf jedes dieser Elemente. Das Ereignis bleibt also nicht im Button. Es beginnt bei event.target und steigt durch die Elternelemente bis zu document und window auf (Bubbling). Jedes Elternelement mit einem click-Listener hört es.

document.querySelector('ul').addEventListener('click', (event) => {
console.log(event.target); // der Button, auf den geklickt wurde
console.log(event.currentTarget); // <ul>: das Element mit dem Listener
});

Die meisten Ereignisse steigen auf: click, keydown, input, submit. Einige nicht: focus und blur. Dafür gibt es focusin und focusout, die aufsteigen.

Sieh es dir an

Sieh den Klick aufsteigen

Links ist der Baum des Katalogs. Der Listener sitzt auf <ul>, und der Klick geht auf den Button der Milch.

const list = document.querySelector('#products');
list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const item = button.closest('li');
console.log('Hinzufügen', item.dataset.id);
});
Schritt für Schritt, in Worten
  1. Es gibt einen einzigen Listener, und er sitzt auf der Liste <ul>, nicht auf den Buttons. Er wartet auf click.
  2. Die Käuferin klickt bei der Milch auf „Hinzufügen“. Das Ereignis beginnt beim innersten Element unter dem Cursor: dem Button. Das ist event.target.
  3. Dann steigt es auf: vom Button zum Elternelement <li>, dann zu <ul>. Jedes Elternelement hört den Klick ebenfalls.
  4. <ul> hat einen Listener, und die Funktion läuft los. event.currentTarget ist <ul>, das Element mit dem Listener. event.target bleibt der Button.
  5. closest('button') beginnt bei event.target selbst und geht nach oben. Das Ziel ist ein Button, also wird er sofort gefunden.
  6. closest('li') geht vom Button nach oben und hält beim ersten <li> an: der Milch. Sein data-id sagt, um welches Produkt es sich handelt.
  7. dataset.id ist "2". Der Warenkorb weiß, was er hinzufügen soll.
  8. Das Ereignis steigt weiter bis <main>, <body>, document und window. Dort gibt es keine Listener, und sonst passiert nichts.
  9. Ein Klick auf den Namen des Brots steigt ebenfalls bis <ul> auf. Aber das Ziel ist <h2>: closest('button') findet über ihm keinen Button und gibt null zurück, und die Funktion hört auf.
  10. Ein Listener bedient jeden Button in der Liste, auch die, die später dazukommen: Sie sind ebenfalls in <ul>. Das heißt Delegation.

Auch ein Klick auf den Namen des Brots erreicht <ul>. Deshalb prüft der Listener zuerst, ob auf einen Button geklickt wurde.

Die Idee

Delegation

Statt eines Listeners pro Button setzt du einen auf das gemeinsame Elternelement. Er findet über event.target heraus, welcher Button geklickt wurde. Das heißt Delegation.

list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const id = button.closest('li').dataset.id;
// …Produkt id hinzufügen
});
  • closest('button') beginnt beim Element selbst und geht nach oben bis zum ersten, das zum Selektor passt. Gibt es keins, kommt null. Hier schützt es vor einem Klick auf den Namen oder die leere Fläche im <li>.
  • Die Daten des Produkts stehen am <li> (data-id), nicht am Button. closest('li') kommt dorthin.

Das bringt zwei Vorteile. Neue Produkte funktionieren ohne Zusatzaufwand: Sie sind auch in der Liste. Und es gibt einen einzigen Listener, egal wie viele Produkte es sind.

Probier es aus

Repariere den Katalog

Repariere den Katalog mit einem einzigen Listener auf #products. „Hinzufügen“ bei jedem Produkt, auch bei dem, das mit „Mehr anzeigen“ dazukommt, erhöht den Warenkorb und schreibt „Produkt hinzugefügt“ und das data-id des Produkts in die Konsole. Ein Klick auf den Namen eines Produkts fügt nichts hinzu.

HTML, CSS, JS

Die Idee

Die Standardaktion

Manche Ereignisse haben eine Aktion, die der Browser nach den Listenern von selbst ausführt. Ein Klick auf einen Link öffnet dessen Adresse. submit bei einem Formular sendet die Felder ab und lädt die neue Seite. Das ist die Standardaktion (default action).

Das Suchformular hier zeigt das Ergebnis auf der Seite. Probier es aus.

HTML, CSS, JS

Der Listener schreibt „Du suchst: Milch“, aber dann sendet der Browser das Formular ab. In einem echten Browser lädt die Seite neu, mit ?q=Milch in der Adresse, und das Ergebnis ist weg. Die Sandbox ist eine einzige Seite: Sie verhindert das Absenden und schreibt „Das Formular wurde gesendet“ in die Konsole.

Füge am Anfang der Funktion event.preventDefault(); hinzu und führ sie noch einmal aus. Die Zeile in der Konsole ist weg: Die Aktion ist gestoppt, und der Listener macht trotzdem seine Arbeit. Dasselbe gilt für einen Link, der etwas innerhalb der Seite öffnet, statt zu einer anderen Adresse zu gehen.

event.stopPropagation() stoppt das Aufsteigen. Du brauchst es selten, und meistens ist es ein schlechtes Zeichen. Es stoppt auch Listener, von denen der Code nichts weiß: die Delegation von oben, das Schließen eines Menüs bei einem Klick daneben, das Zählen von Klicks. Wenn ein Listener „zu viel hört“, prüfe event.target, so wie es closest bei der Delegation tut.

Probier es aus

Finde den Fehler

Der Listener zeigt das Ergebnis an, aber das Formular wird trotzdem abgesendet.

Wähle die Zeile mit dem Fehler.

Die Idee

input oder change

Felder haben zwei Ereignisse für eine Änderung:

  • input kommt bei jeder Änderung des Werts: jeder Buchstabe, jedes Löschen, jedes Einfügen.
  • change kommt, wenn die Käuferin fertig ist. Bei einem Textfeld ist das, wenn sie es verlässt. Bei einer Checkbox, einem Radiobutton und einem Dropdown ist es gleich nach der Auswahl.

Für eine Suche, die beim Tippen filtert, nimmst du input. Für ein Dropdown „Nach Preis sortieren“ nimmst du change.

Genauer, für Neugierige

Ein drittes Argument. addEventListener('click', f, { once: true }) hört nur einmal zu. removeEventListener('click', f) entfernt den Listener, aber nur, wenn du dieselbe Funktion übergibst. Eine noch einmal hingeschriebene Pfeilfunktion ist eine andere Funktion.

onclick. button.onclick = f funktioniert auch, ist aber ein einziges Feld: Ein zweites onclick löscht das erste. addEventListener erlaubt viele Listener. Das Attribut onclick="…" im HTML vermischt den Code mit der Seite, und eine strenge Sicherheitsrichtlinie (CSP) blockiert es.

Bubbling und Capturing. Der Weg des Ereignisses hat noch eine andere Richtung: zuerst von window hinunter zum Ziel (Capturing), dann hinauf (Bubbling). addEventListener('click', f, { capture: true }) hört auf dem Weg nach unten. Fast immer willst du das Bubbling.

Die Tastatur. Ein <button> bekommt click auch von Enter und Leertaste. Deshalb funktioniert click bei einem Button auch mit der Tastatur. keydown auf einem <div> ersetzt das nicht (siehe die Lektion über Barrierefreiheit).

event.key und event.code. key ist das Zeichen ('a' auf einer QWERTZ-Tastatur, 'q' für dieselbe Taste auf einer französischen AZERTY-Tastatur), und code ist der Ort der Taste ('KeyA' bei beiden). Für Kürzel wie Escape und Enter nimmst du key.

Übung

Live-Suche

Baue die Suche so, dass sie die Liste filtert, während die Käuferin tippt: Es werden nur die Produkte angezeigt, die den Text enthalten, egal in welcher Groß- und Kleinschreibung. Ein verstecktes Produkt hat hidden. #found zeigt, wie viele angezeigt werden („Gefunden: 2“). Escape im Feld leert es und zeigt alle an. Enter sendet das Formular nicht ab.

HTML, CSS, JS

Test

Teste dich

1. Der Listener sitzt auf <ul>, und die Käuferin klickt auf einen Button in einem <li>. Was sind event.target und event.currentTarget?
2. Was ist an button.addEventListener('click', save()) falsch?
3. Warum funktioniert die Delegation auch für Produkte, die nach dem Laden der Seite dazukommen?
4. Ein Suchformular hat einen submit-Listener, der das Ergebnis anzeigt. Wozu brauchst du event.preventDefault()?
5. Eine Suche, die die Liste filtert, während die Käuferin tippt. Welches Ereignis?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·