Zum Inhalt springen
aviral gupta

// B6.4 · ca. 32 Min. · Einstieg

Bubbling und Event-Delegation

Nach dieser Lektion kennen Sie den Weg eines Klicks durch die Seite, unterscheiden target von currentTarget, und ein einziger Listener auf einer Liste bedient jeden Eintrag darin, auch später hinzugefügte.

Lektion 4 von 5 in B6 Das DOM und Ereignisse

Danach können Sie

  • Ein Event durch Capture-, Target- und Bubble-Phase verfolgen und vorhersagen, in welcher Reihenfolge Listener auf Vorfahren laufen
  • event.target von event.currentTarget unterscheiden und stopPropagation() nur bei einem echten Konflikt einsetzen
  • Viele Elemente, auch später hinzugefügte, mit einem delegierten Listener über closest() und contains() steuern
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B6.3. Was gibt dieser Code aus?

    const button = document.querySelector('#like');
    let n = 0;
    function count() {
      n += 1;
    }
    button.addEventListener('click', count);
    button.click();
    button.removeEventListener('click', count);
    button.click();
    console.log(n);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. Der Listener sitzt auf der Liste, der Klick landet auf dem Eintrag darin. Was gibt dieser Code aus?

    const list = document.querySelector('#list');
    list.addEventListener('click', (event) => {
      console.log(event.target.tagName, event.currentTarget.tagName);
    });
    document.querySelector('#first').click();
  3. Üben · Aufgabe 3 von 7

    Der Klick landete auf einem <b> in einem <span> im li. Ergänzen Sie die Methode, die vom geklickten Element nach oben bis zum nächsten li geht.

    const item = event.target.____('li');
    const item = event.target.('li');
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jedem Abschnitt der Reise eines Events zu, was darin passiert.

  5. Üben · Aufgabe 5 von 7

    Der Button #go liegt im div #box. Was gibt dieser Code aus?

    const box = document.querySelector('#box');
    const go = document.querySelector('#go');
    const order = [];
    box.addEventListener('click', () => order.push('box bubble'));
    box.addEventListener('click', () => order.push('box capture'), { capture: true });
    go.addEventListener('click', () => order.push('button'));
    go.click();
    console.log(order.join(', '));
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Jeder Eintrag bekommt einen Listener, dann kommt ein neuer Eintrag hinzu, und beide werden geklickt. Was gibt dieser Code aus?

    const list = document.querySelector('#list');
    let n = 0;
    for (const li of list.querySelectorAll('li')) {
      li.addEventListener('click', () => {
        n += 1;
      });
    }
    const coffee = document.createElement('li');
    coffee.textContent = 'Coffee';
    list.append(coffee);
    coffee.click();
    list.querySelector('li').click();
    console.log(n);
  7. Anwenden · Aufgabe 7 von 7

    Mini-Aufgabe: entfernbare Tags. Schreiben Sie eine index.html mit einer leeren Liste #tags und einem span #count. Halten Sie in main.js die Tags in einem Array, ['js', 'dom', 'events'], und schreiben Sie render(): Jeder Tag wird ein li mit seinem Text und einem kleinen Button mit data-index und einem aria-label wie "Remove js"; #count zeigt, wie viele Tags es gibt. Fügen Sie der Liste einen einzigen Klick-Listener hinzu, nicht einen pro Button: Finden Sie den Button mit closest, prüfen Sie, dass er in der Liste liegt, entfernen Sie den Tag aus dem Array und rufen Sie render() erneut auf. Probieren Sie es über einen lokalen Webserver aus.

    Prüfen Sie Ihr Ergebnis anhand dieser Liste

Selbst programmieren

Lesen Sie das ausgearbeitete Beispiel und lösen Sie dann die Übungen. Ihr Code läuft in Ihrem Browser oder auf Ihrem Computer und wird nie hochgeladen.

Ausgearbeitetes Beispiel

Ein Listener für eine ganze To-do-Liste

Die Seite hat eine leere Liste. main.js hält die Aufgaben in einem Array und baut die Einträge mit render(), wie in Lektion B6.2. Ein einziger Klick-Listener auf der Liste bedient jeden Done-Button: Er findet den Button mit closest, prüft, dass er zur Liste gehört, schaltet die Aufgabe um und rendert neu. Zwei Listener auf body zeigen die Reihenfolge der Phasen. Eine dritte Aufgabe kommt nach dem Listener hinzu, und das Skript klickt dann ihren Done-Button und die Liste selbst, so wie es die Tests der Übungen tun.

main.js

const list = document.querySelector('#list');
const tasks = [
  { text: 'Water the plants', done: false },
  { text: 'Buy bread', done: false }
];

function render() {
  const items = tasks.map((task, index) => {
    const li = document.createElement('li');
    li.dataset.index = String(index);
    if (task.done) li.classList.add('done');
    const text = document.createElement('span');
    text.textContent = task.text;
    const button = document.createElement('button');
    button.textContent = 'Done';
    li.append(text, ' ', button);
    return li;
  });
  list.replaceChildren(...items);
}

// One listener on the list handles every button, now and later.
list.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button || !list.contains(button)) return; // not a button of this list
  const index = Number(button.closest('li').dataset.index);
  tasks[index].done = !tasks[index].done;
  console.log('toggled', index, '| target:', event.target.tagName, '| currentTarget:', event.currentTarget.id);
  render();
});

// Two listeners on body show the order: capture first, bubble last.
document.body.addEventListener('click', () => console.log('body, capture phase'), { capture: true });
document.body.addEventListener('click', () => console.log('body, bubble phase'));

render();
tasks.push({ text: 'Call Mum', done: false }); // added after the listener
render();

// The script clicks its own page, the way the exercise tests do.
list.querySelector('[data-index="2"] button').click();
list.click(); // the list itself, not a button: ignored
console.log('items:', list.children.length);

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Bubbling</title>
</head>
<body>
  <main>
    <h1>Today</h1>
    <ul id="list"></ul>
  </main>
  <script type="module" src="main.js"></script>
</body>
</html>

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Ausgabe

body, capture phase
toggled 2 | target: BUTTON | currentTarget: list
body, bubble phase
body, capture phase
body, bubble phase
items: 3
<!-- #list -->
<ul id="list"><li data-index="0"><span>Water the plants</span> <button>Done</button></li><li data-index="1"><span>Buy bread</span> <button>Done</button></li><li data-index="2" class="done"><span>Call Mum</span> <button>Done</button></li></ul>
  • Der Capture-Listener auf body lief zuerst, auf dem Weg nach unten; der Listener der Liste lief, als der Klick aufstieg; der einfache Listener auf body lief zuletzt.
  • Call Mum kam nach dem Listener hinzu und funktioniert trotzdem: Der Listener sitzt auf der Liste, nicht auf den Einträgen.
  • target ist der geklickte Button, currentTarget die Liste, deren Listener lief.
  • Der Klick auf die Liste selbst lief ebenfalls durch body, aber closest fand keinen Button, also kehrte der Listener der Liste früh zurück.
Ändern und ausführen

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Übungen

Übung 1 von 2

Ein Listener für eine Farbpalette

Ersetzen Sie die drei Listener des Starters durch einen einzigen Klick-Listener auf #palette. Ein Klick auf einen Farb-Button oder auf den Punkt darin zeigt in #chosen "Chosen: " plus das data-color des Buttons und gibt diesem Button, und nur diesem, die Klasse active. Ein Klick auf die Palette zwischen den Buttons ändert nichts. Auch ein später zur Palette hinzugefügter Button muss funktionieren. Der Starter liest event.target.dataset.color, das undefined ist, wenn der Klick auf dem Punkt landet, und neue Buttons bekommen keinen Listener.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Der Punkt ergibt "Chosen: undefined", nichts bekommt die Klasse active, und der Button Gold tut nichts.

  2. Hinweis 2

    Ein Listener: palette.addEventListener('click', (event) => { … }). Darin const button = event.target.closest('button'), und return, wenn !button || !palette.contains(button).

  3. Hinweis 3

    Entfernen Sie active zuerst in einer Schleife von jedem Button, fügen Sie es dann dem geklickten hinzu, und lesen Sie button.dataset.color, nicht event.target.dataset.color.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

const palette = document.querySelector('#palette');
const chosen = document.querySelector('#chosen');

palette.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button || !palette.contains(button)) return;
  for (const other of palette.querySelectorAll('button')) {
    other.classList.remove('active');
  }
  button.classList.add('active');
  chosen.textContent = 'Chosen: ' + button.dataset.color;
});
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

const palette = document.querySelector('#palette');
const chosen = document.querySelector('#chosen');

// TODO: replace these listeners with one listener on the palette.
for (const button of palette.querySelectorAll('button')) {
  button.addEventListener('click', (event) => {
    chosen.textContent = 'Chosen: ' + event.target.dataset.color;
  });
}

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const button = (page, color) => page.document.querySelector('#palette [data-color="' + color + '"]');
const chosen = (page) => page.document.querySelector('#chosen').textContent;
const active = (page) => [...page.document.querySelectorAll('#palette .active')].map((b) => b.dataset.color);

test('Ein Klick auf einen Button zeigt seine Farbe', async () => {
  const page = await loadPage();
  button(page, 'red').click();
  assert.equal(chosen(page), 'Chosen: red', 'nach dem Klick auf Red zeigt die Seite ' + JSON.stringify(chosen(page)));
});

test('Ein Klick auf den Punkt im Button zählt für diesen Button', async () => {
  const page = await loadPage();
  button(page, 'green').querySelector('.dot').click();
  assert.equal(chosen(page), 'Chosen: green', 'nach dem Klick auf den Punkt in Green zeigt die Seite ' + JSON.stringify(chosen(page)));
});

test('Nur der geklickte Button hat die Klasse active', async () => {
  const page = await loadPage();
  button(page, 'red').click();
  button(page, 'blue').click();
  assert.deepEqual(active(page), ['blue'], 'die Buttons mit der Klasse active sind ' + JSON.stringify(active(page)));
});

test('Ein Klick auf die Palette selbst ändert nichts', async () => {
  const page = await loadPage();
  page.document.querySelector('#palette').click();
  assert.equal(chosen(page), 'No colour yet', 'nach einem Klick zwischen die Buttons zeigt die Seite ' + JSON.stringify(chosen(page)));
  assert.deepEqual(active(page), [], 'die Buttons mit der Klasse active sind ' + JSON.stringify(active(page)));
});

test('Auch ein später hinzugefügter Button funktioniert', async () => {
  const page = await loadPage();
  const extra = page.document.createElement('button');
  extra.type = 'button';
  extra.dataset.color = 'gold';
  extra.textContent = 'Gold';
  page.document.querySelector('#palette').append(extra);
  extra.click();
  assert.equal(chosen(page), 'Chosen: gold', 'nach dem Klick auf den neuen Button Gold zeigt die Seite ' + JSON.stringify(chosen(page)));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Palette</title>
</head>
<body>
  <div id="palette">
    <button type="button" data-color="red"><span class="dot"></span> Red</button>
    <button type="button" data-color="green"><span class="dot"></span> Green</button>
    <button type="button" data-color="blue"><span class="dot"></span> Blue</button>
  </div>
  <p id="chosen">No colour yet</p>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Übung 2 von 2

Eine Checkliste mit Remove und Done

Die Checkliste steht im Abschnitt #today, der jeden Klick darin zählt; dieser Zähler soll weiter funktionieren. Schreiben Sie einen einzigen Klick-Listener auf #list: Ein Klick auf den Remove-Button eines Eintrags entfernt diesen Eintrag aus items; ein Klick an anderer Stelle eines Eintrags schaltet sein done um; ein Klick auf die Liste zwischen den Einträgen ändert nichts. Rufen Sie nach jeder Änderung render() auf. Der Starter stoppt die Ausbreitung, sodass der Zähler stehen bleibt, entfernt den Eintrag bei jedem Klick, und bei einem Klick zwischen die Einträge klettert closest('li') über die Liste hinaus zu #today und entfernt den ersten Eintrag.

Tab rückt ein, Umschalt+Tab rückt aus. Um den Editor mit der Tastatur zu verlassen, drücken Sie Esc und dann Tab.

Beim ersten Ausführen lädt Ihr Browser den JavaScript-Runner herunter (bis zu 0.1 MB) und speichert ihn im Cache. Ihr Code läuft in der eigenen Engine Ihres Browsers und bleibt auf Ihrem Gerät.

Ihre Seite

Ihre Seite läuft in einem abgeschotteten Rahmen. Sie hat keinen Zugriff auf diese Website, und ihr localStorage liegt nur für diesen Lauf im Speicher: Der nächste Lauf beginnt leer.

Führen Sie das Programm aus, um seine Seite hier zu sehen.

Hinweise
  1. Hinweis 1

    Führen Sie die Tests aus: Der Zähler bleibt bei 0, ein Klick auf einen Text entfernt den Eintrag, und ein Klick zwischen die Einträge entfernt den ersten.

  2. Hinweis 2

    Löschen Sie event.stopPropagation(). Dann const li = event.target.closest('li'), und return, wenn !li || !list.contains(li): Die Liste liegt in einem anderen li, #today.

  3. Hinweis 3

    Unterscheiden Sie mit event.target.closest('.remove') einen Klick auf Remove von jedem anderen Klick auf den Eintrag, ändern Sie items und rufen Sie render() auf.

Eine Lösung zeigen

Ein möglicher Lösungsweg. Ihrer kann anders aussehen und trotzdem alle Prüfungen bestehen.

const list = document.querySelector('#list');
const items = [
  { text: 'Water the plants', done: false },
  { text: 'Buy bread', done: false },
  { text: 'Call Mum', done: false }
];

function render() {
  const lis = items.map((item, index) => {
    const li = document.createElement('li');
    li.dataset.index = String(index);
    if (item.done) li.classList.add('done');
    const text = document.createElement('span');
    text.textContent = item.text;
    const remove = document.createElement('button');
    remove.type = 'button';
    remove.className = 'remove';
    remove.textContent = 'Remove';
    li.append(text, ' ', remove);
    return li;
  });
  list.replaceChildren(...lis);
}

// Given: counts every click inside the Today section. Keep it working.
let clicks = 0;
document.querySelector('#today').addEventListener('click', () => {
  clicks += 1;
  document.querySelector('#clicks').textContent = String(clicks);
});

list.addEventListener('click', (event) => {
  const li = event.target.closest('li');
  if (!li || !list.contains(li)) return;
  const index = Number(li.dataset.index);
  if (event.target.closest('.remove')) {
    items.splice(index, 1);
  } else {
    items[index].done = !items[index].done;
  }
  render();
});

render();
Auf dem eigenen Computer ausführen

Installieren Sie ECMAScript 2026 oder neuer. Speichern Sie diese Dateien in einem Ordner, öffnen Sie dort ein Terminal und führen Sie die Befehle unten aus.

main.js

const list = document.querySelector('#list');
const items = [
  { text: 'Water the plants', done: false },
  { text: 'Buy bread', done: false },
  { text: 'Call Mum', done: false }
];

function render() {
  const lis = items.map((item, index) => {
    const li = document.createElement('li');
    li.dataset.index = String(index);
    if (item.done) li.classList.add('done');
    const text = document.createElement('span');
    text.textContent = item.text;
    const remove = document.createElement('button');
    remove.type = 'button';
    remove.className = 'remove';
    remove.textContent = 'Remove';
    li.append(text, ' ', remove);
    return li;
  });
  list.replaceChildren(...lis);
}

// Given: counts every click inside the Today section. Keep it working.
let clicks = 0;
document.querySelector('#today').addEventListener('click', () => {
  clicks += 1;
  document.querySelector('#clicks').textContent = String(clicks);
});

// TODO: Remove removes its item; a click elsewhere on an item toggles done.
list.addEventListener('click', (event) => {
  event.stopPropagation(); // keep the click to ourselves
  const li = event.target.closest('li');
  items.splice(Number(li.dataset.index), 1);
  render();
});

render();

main.test.js

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {loadPage} from './learnrun.js';

const item = (page, n) => page.document.querySelectorAll('#list li')[n];
const texts = (page) => [...page.document.querySelectorAll('#list li span')].map((span) => span.textContent);
const done = (page) => [...page.document.querySelectorAll('#list li')].map((li) => li.classList.contains('done'));

test('Remove entfernt seinen Eintrag', async () => {
  const page = await loadPage();
  item(page, 1).querySelector('.remove').click();
  assert.deepEqual(texts(page), ['Water the plants', 'Call Mum'], 'nach dem Entfernen von Buy bread zeigt die Liste ' + JSON.stringify(texts(page)));
});

test('Ein Klick auf den Text markiert den Eintrag als erledigt', async () => {
  const page = await loadPage();
  item(page, 0).querySelector('span').click();
  assert.deepEqual(texts(page), ['Water the plants', 'Buy bread', 'Call Mum'], 'nach einem Klick auf einen Text zeigt die Liste ' + JSON.stringify(texts(page)));
  assert.deepEqual(done(page), [true, false, false], 'nach dem Klick auf den ersten Text ist done ' + JSON.stringify(done(page)));
});

test('Ein zweiter Klick auf den Text hebt die Markierung auf', async () => {
  const page = await loadPage();
  item(page, 2).querySelector('span').click();
  item(page, 2).querySelector('span').click();
  assert.deepEqual(done(page), [false, false, false], 'nach zwei Klicks auf den letzten Text ist done ' + JSON.stringify(done(page)));
});

test('Ein Klick auf die Liste selbst ändert nichts', async () => {
  const page = await loadPage();
  page.document.querySelector('#list').click();
  assert.deepEqual(texts(page), ['Water the plants', 'Buy bread', 'Call Mum'], 'nach einem Klick zwischen die Einträge zeigt die Liste ' + JSON.stringify(texts(page)));
  const today = page.document.querySelector('#today').classList.contains('done');
  assert.equal(today, false, 'der Abschnitt Today hat die Klasse done bekommen');
});

test('Entfernen funktioniert auch nach dem Neuaufbau der Liste', async () => {
  const page = await loadPage();
  item(page, 0).querySelector('.remove').click();
  item(page, 0).querySelector('.remove').click();
  assert.deepEqual(texts(page), ['Call Mum'], 'nach zweimaligem Entfernen des ersten Eintrags zeigt die Liste ' + JSON.stringify(texts(page)));
});

test('Klicks in der Liste erreichen weiter den Zähler des Abschnitts', async () => {
  const page = await loadPage();
  item(page, 0).querySelector('span').click();
  item(page, 1).querySelector('.remove').click();
  const clicks = page.document.querySelector('#clicks').textContent;
  assert.equal(clicks, '2', 'nach zwei Klicks in der Liste zeigt der Zähler ' + JSON.stringify(clicks));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Checklist</title>
</head>
<body>
  <ul id="sections">
    <li id="today">Today
      <ul id="list"></ul>
    </li>
  </ul>
  <p>Clicks in Today: <span id="clicks">0</span></p>
  <script type="module" src="main.js"></script>
</body>
</html>

package.json

{
  "type": "module"
}

package.json sagt Node.js, dass die .js-Dateien Module sind; lassen Sie die Datei im Ordner.

Programm ausführen:

Um die Seite auf Ihrem Computer auszuführen, öffnen Sie index.html über einen lokalen Webserver, nicht als Datei: Browser führen Modul-Skripte von file://-Adressen nicht aus. Für die Prüfungen installieren Sie zuerst jsdom (die Befehle unten).

Prüfungen ausführen (learnrun.js muss im selben Ordner liegen):

npm install jsdom@28.1.0
node --test
learnrun.js herunterladen

Häufige Fehler

closest() verwenden, ohne auf null zu prüfen

const list = document.querySelector('#list');
list.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  console.log('remove', button.closest('li').dataset.index);
});
// A click on the list itself, between the items:
list.click();

Was Node.js ausgibt

TypeError: Cannot read properties of null (reading 'closest')

Warum, und die Lösung

Ein delegierter Listener läuft bei jedem Klick im Container, nicht nur bei Klicks auf Buttons. Landet der Klick zwischen den Einträgen, findet closest('button') keinen Button und liefert null. Prüfen Sie zuerst und kehren Sie früh zurück: if (!button || !list.contains(button)) return;. Firefox und Safari formulieren diese Meldung anders.

contains() einen Selektor übergeben

const list = document.querySelector('#list');
list.addEventListener('click', (event) => {
  const button = event.target.closest('button');
  if (!button || !list.contains('button')) return;
  console.log('remove', button.closest('li').dataset.index);
});
list.querySelector('button').click();

Was Node.js ausgibt

TypeError: Failed to execute 'contains' on 'Node': parameter 1 is not of type 'Node'.

Warum, und die Lösung

closest nimmt einen CSS-Selektor, contains aber einen Knoten: das Element, das Sie gefunden haben. Schreiben Sie list.contains(button), ohne Anführungszeichen. Es liefert true, wenn button die Liste selbst ist oder irgendwo darin liegt.

closest() auf dem Event statt auf seinem Ziel aufrufen

const list = document.querySelector('#list');
list.addEventListener('click', (event) => {
  const item = event.closest('li');
  console.log('clicked', item.dataset.index);
});
list.querySelector('button').click();

Was Node.js ausgibt

TypeError: event.closest is not a function

Warum, und die Lösung

Das Event-Objekt beschreibt, was passiert ist; es ist kein Element und hat daher kein closest. Beginnen Sie bei dem Element, auf dem geklickt wurde: event.target.closest('li'). Firefox und Safari formulieren diese Meldung anders.

JavaScript im Browser: die eigene Engine Ihres Browsers in einem abgeschotteten Worker. Syntaxfehler findet acorn 8.18.0, MIT. Lizenz und Quellcode

Abschlussquiz

5 Fragen, ohne Hinweise. Ab 80 % ist die Lektion abgeschlossen.

Erledigen Sie zuerst alle Aufgaben oben, um das Abschlussquiz freizuschalten.

Problem melden

Etwas ist falsch oder unklar? Beschreiben Sie es kurz, dann wird es geprüft und korrigiert.

#

Mindestens 20 Zeichen.

Nur, wenn Sie eine Antwort wünschen.

Kernideen

Ein Event wandert: Capture, Target, Bubble

Ein Klick betrifft nie nur ein Element. Der Browser schickt das Event auf einem Weg durch den Baum, in drei Phasen. Capture: von window hinunter durch jeden Vorfahren bis zum Elternelement des geklickten Elements. Target: am Element selbst. Bubble: wieder hinauf über das Elternelement, dessen Elternelement und weiter bis document und window. addEventListener hört standardmäßig in der Bubble-Phase, ein Listener auf einer Liste läuft also auch, wenn ein Eintrag darin geklickt wird. Mit { capture: true } hört er stattdessen auf dem Weg nach unten und läuft vor den Listenern des Ziels. Ein Event, das Sie mit new Event() bauen, steigt nur auf, wenn Sie { bubbles: true } angeben.

target, currentTarget und wann man stoppt

event.target ist das Element, auf dem das Event begann, das innerste geklickte; es bleibt auf dem ganzen Weg gleich. event.currentTarget ist das Element, dessen Listener gerade läuft, es ändert sich also beim Aufsteigen: der Button, dann die Liste, dann body. event.stopPropagation() beendet die Reise: Listener weiter oben auf dem Weg laufen nicht mehr. Die Standardaktion bricht es nicht ab; das tut preventDefault(). Setzen Sie es selten ein, für einen echten Konflikt, etwa einen Klick in einer Box, der die Box nicht zugleich schließen soll. Als Gewohnheit zerstört es Code, den Sie nicht sehen, etwa ein Menü, das sich bei jedem Klick auf die Seite schließt, oder einen Klickzähler weiter oben.

Event-Delegation

Statt eines Listeners pro Eintrag setzen Sie einen Listener auf den Container und lassen die Klicks zu ihm aufsteigen. Darin finden Sie den Eintrag über event.target: event.target.closest("li") beginnt beim geklickten Element selbst und geht nach oben bis zum nächsten Treffer, oder liefert null. Der Klick kann auf einem span im li landen, deshalb ist closest sicherer, als nur event.target anzusehen. Prüfen Sie dann list.contains(li): closest kann über den Container hinaus klettern und ein li außerhalb finden. Kehren Sie früh zurück, wenn es keinen Treffer gibt. Delegation erfasst auch später hinzugefügte oder von render() neu gebaute Elemente, denn der Listener sitzt auf dem Container, der bleibt. Legen Sie die Daten eines Eintrags in data-*-Attribute.

Quellen

Zuletzt geprüft am 4. Oktober 2026