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.
Konsole
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.targetist das Element, an dem das Ereignis passiert ist: der Button, auf den geklickt wurde.event.currentTargetist das Element, dessen Listener gerade läuft.event.keyist beikeydowndie 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.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Dobrudzha-Brot</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<h1>Dobrudzha-Brot</h1>
<p>
<button type="button" id="minus" aria-label="Weniger">−</button>
<output id="qty">1</output>
<button type="button" id="plus" aria-label="Mehr">+</button>
</p>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; padding: 16px; }
button { font-size: 16px; padding: 6px 14px; }
output { display: inline-block; min-width: 2em; text-align: center; font-size: 20px; }
script.jsconst qty = document.querySelector('#qty');
let count = 1;
document.querySelector('#plus').addEventListener('click', () => {
count++;
qty.textContent = count;
});
document.querySelector('#minus').addEventListener('click', () => {
if (count > 1) count--;
qty.textContent = count;
});
Jeder Button hat einen eigenen Listener mit einer Funktion, die count ändert und mit textContent anzeigt. Bei „−“ verhindert die Prüfung if (count > 1), dass es auf 0 fällt. Steht die Funktion für sich, übergibst du sie ohne Klammern: addEventListener('click', plus). Mit plus() wird sie einmal beim Laden der Seite aufgerufen, die Menge wird 2, und es gibt keinen Listener.
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
- Es gibt einen einzigen Listener, und er sitzt auf der Liste <ul>, nicht auf den Buttons. Er wartet auf click.
- 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.
- Dann steigt es auf: vom Button zum Elternelement <li>, dann zu <ul>. Jedes Elternelement hört den Klick ebenfalls.
- <ul> hat einen Listener, und die Funktion läuft los. event.currentTarget ist <ul>, das Element mit dem Listener. event.target bleibt der Button.
- closest('button') beginnt bei event.target selbst und geht nach oben. Das Ziel ist ein Button, also wird er sofort gefunden.
- 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.
- dataset.id ist "2". Der Warenkorb weiß, was er hinzufügen soll.
- Das Ereignis steigt weiter bis <main>, <body>, document und window. Dort gibt es keine Listener, und sonst passiert nichts.
- 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.
- 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, kommtnull. 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.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Katalog</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<p id="count">Warenkorb: 0</p>
<ul id="products">
<li data-id="1">
<h2>Dobrudzha-Brot</h2>
<button type="button">Hinzufügen</button>
</li>
<li data-id="2">
<h2>Frische Milch 3 %</h2>
<button type="button">Hinzufügen</button>
</li>
</ul>
<button type="button" id="more">Mehr anzeigen</button>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; padding: 16px; }
button { font-size: 16px; padding: 6px 14px; }
#products { list-style: none; padding: 0; }
#products li { display: flex; align-items: center; gap: 12px; border-bottom: 1px solid #ddd; }
#products h2 { font-size: 18px; flex: 1; }
script.jsconst count = document.querySelector('#count');
let items = 0;
document.querySelector('#products').addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
items++;
count.textContent = `Warenkorb: ${items}`;
console.log('Produkt hinzugefügt', button.closest('li').dataset.id);
});
document.querySelector('#more').addEventListener('click', () => {
const li = document.createElement('li');
li.dataset.id = '3';
const title = document.createElement('h2');
title.textContent = 'Käse';
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Hinzufügen';
li.append(title, button);
document.querySelector('#products').append(li);
});
Der Listener sitzt auf #products, also hört er auch den Käse-Button. event.target.closest('button') ist null, wenn auf <h2> geklickt wurde, und die Funktion endet. event.target.dataset.id ist undefined: data-id steht am <li>, also muss es button.closest('li').dataset.id sein.
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.
Konsole
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.
event.preventDefault ohne Klammern liest die Methode nur und ruft sie nicht auf. Es muss event.preventDefault(); heißen.
Die Idee
input oder change
Felder haben zwei Ereignisse für eine Änderung:
inputkommt bei jeder Änderung des Werts: jeder Buchstabe, jedes Löschen, jedes Einfügen.changekommt, 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.
Konsole
Lösung
index.html<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Katalog</title>
<link rel="stylesheet" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<form id="search">
<label>Suche <input id="q" autocomplete="off"></label>
</form>
<p id="found">Gefunden: 4</p>
<ul id="products">
<li>Dobrudzha-Brot</li>
<li>Frische Milch 3 %</li>
<li>Schokomilch</li>
<li>Käse</li>
</ul>
</body>
</html>
style.cssbody { font-family: system-ui, sans-serif; padding: 16px; }
button { font-size: 16px; padding: 6px 14px; }
script.jsconst input = document.querySelector('#q');
const found = document.querySelector('#found');
const items = document.querySelectorAll('#products li');
function filter() {
const q = input.value.trim().toLowerCase();
let shown = 0;
items.forEach((li) => {
const match = li.textContent.toLowerCase().includes(q);
li.hidden = !match;
if (match) shown++;
});
found.textContent = `Gefunden: ${shown}`;
}
input.addEventListener('input', filter);
input.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
input.value = '';
filter();
}
});
document.querySelector('#search').addEventListener('submit', (event) => {
event.preventDefault();
});
Die Funktion filter geht jedes <li> durch, vergleicht seinen Text mit der Suche (beide mit toLowerCase()) und setzt li.hidden. Sie läuft bei input: bei jedem Buchstaben. Mit change ändert sich die Liste nicht, während die Käuferin tippt. Escape ist keydown mit event.key === 'Escape': ein leeres Feld und noch einmal filter(). Die Lösung stoppt Enter auch mit preventDefault() bei submit, damit das Formular nicht abgesendet wird. Die Prüfung drückt Enter und schaut in die Konsole: Die Sandbox lädt die Seite nicht neu, aber sie schreibt, wenn das Formular abgesendet wird.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Text, Links, Bilder, Tabellen · Formulare · Selektoren und die Kaskade · Das DOM: die Seite aus JavaScript
- Geht weiter in: Eine Liste aus Daten · Daten aus einer API · Die erste Komponente · Listen und Keys
- Aufgaben zum Thema: JavaScript in der Seite