Zum Inhalt springen
aviral gupta

// B6.3 · ca. 30 Min. · Einstieg

Listener und das Event-Objekt

Nach dieser Lektion reagiert Ihre Seite auf Klicks, Eingaben und Formulare: Sie fügen Listener hinzu und entfernen sie, lesen das Event-Objekt und verarbeiten ein Formular ohne Neuladen.

Lektion 3 von 5 in B6 Das DOM und Ereignisse

Danach können Sie

  • Listener mit addEventListener statt mit on…-Attributen hinzufügen und sie mit removeEventListener oder der Option once entfernen
  • Das Event-Objekt lesen: type, target, currentTarget und key bei Tastatur-Events
  • input- und submit-Events verarbeiten: input.value lesen, preventDefault() aufrufen und die Falle form.elements.item vermeiden
  1. Aufwärmen · Aufgabe 1 von 7

    Zum Aufwärmen aus Lektion B6.2. Die Liste enthält einen Eintrag. Was gibt dieser Code aus?

    const list = document.querySelector('#list');
    const li = document.createElement('li');
    li.textContent = 'b';
    list.prepend(li);
    console.log(list.textContent, list.children.length);
  2. Vorhersagen · Aufgabe 2 von 7

    Sagen Sie voraus, bevor Sie weiterlesen. Ein Listener zählt input-Events. Was gibt dieser Code aus?

    const field = document.querySelector('#name');
    let n = 0;
    field.addEventListener('input', () => {
      n += 1;
    });
    field.value = 'cat';
    console.log(n, field.value);
  3. Üben · Aufgabe 3 von 7

    Ergänzen Sie die Methode, die count bei jedem Klick ausführen lässt.

    button.____('click', count);
    button.('click', count);
  4. Üben · Aufgabe 4 von 7

    Ordnen Sie jeder Eigenschaft des Event-Objekts zu, was sie enthält.

  5. Üben · Aufgabe 5 von 7

    Der Button ist <button id="buy"><span id="icon">*</span> Buy</button>, und der Klick landet auf dem Icon. Was gibt dieser Code aus?

    const buy = document.querySelector('#buy');
    buy.addEventListener('click', (event) => {
      console.log(event.target.id, event.currentTarget.id);
    });
    document.querySelector('#icon').click();
  6. Denksport · Aufgabe 6 von 7

    Knobelaufgabe. Das Programm entfernt den Listener, so scheint es. Was gibt es aus?

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

    Mini-Aufgabe: ein Abstimmungs-Button und ein Begrüßungsformular. Schreiben Sie eine index.html mit einem Button #vote, einem span #votes, das 0 zeigt, und einem Formular #name-form mit einem input namens who. In main.js erhöht jeder Klick auf den Button um eins und zeigt die Anzahl. Rufen Sie beim submit event.preventDefault() auf, lesen Sie form.elements.who.value, trimmen Sie es und zeigen Sie "Hello, " plus den Namen mit textContent in einem Absatz #hello. 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 Like-Button und ein Suchformular

Die Seite hat einen Like-Button mit Zähler, ein Suchformular und einen Protokoll-Absatz. main.js fügt einen click-Listener hinzu, der zählt, einen zweiten mit { once: true }, einen submit-Listener, der preventDefault() aufruft und die Suche anzeigt, und einen keydown-Listener am Feld. Am Ende drückt das Skript seinen eigenen Button zweimal, schickt das Formular mit requestSubmit() ab und sendet die Taste Escape, so wie es die Tests der Übungen tun, damit Sie sehen, was jeder Listener bekommt.

main.js

const like = document.querySelector('#like');
const likes = document.querySelector('#likes');
const form = document.querySelector('#search');
const query = form.elements.query;
const log = document.querySelector('#log');

let count = 0;
like.addEventListener('click', (event) => {
  count += 1;
  likes.textContent = String(count);
  console.log(event.type, event.target.id, count);
});

// once: this listener is removed after its first call.
like.addEventListener('click', () => console.log('thanks for the first like'), { once: true });

form.addEventListener('submit', (event) => {
  event.preventDefault(); // handle it here: no page reload
  log.textContent = 'Searching for ' + query.value.trim();
  console.log(event.type, event.target.id, event.currentTarget === form);
});

query.addEventListener('keydown', (event) => {
  console.log(event.type, event.key);
});

// The script tries its own page, the way the exercise tests do.
like.click();
like.click();
query.value = '  cats ';
form.requestSubmit();
query.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
console.log('likes shown:', likes.textContent);

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Listeners</title>
</head>
<body>
  <button id="like" type="button">Like</button> <span id="likes">0</span>
  <form id="search">
    <input name="query" aria-label="Search">
    <button>Search</button>
  </form>
  <p id="log">Nothing searched yet.</p>
  <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

click like 1
thanks for the first like
click like 2
submit search true
keydown Escape
likes shown: 2
<!-- #log -->
<p id="log">Searching for cats</p>
  • Beim ersten Klick liefen beide click-Listener, in der Reihenfolge ihres Hinzufügens; der once-Listener lief danach nicht mehr.
  • Bei submit sind target und currentTarget beide das Formular, currentTarget === form ist also true.
  • event.key nennt die Taste: Escape.
  • Der Suchtext wurde getrimmt und mit textContent gezeigt; preventDefault() hat die Seite gehalten.
Ä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

Eine Live-Vorschau für den Status

Während der Nutzer in #status tippt, soll sich die Seite bei jedem input-Event aktualisieren: #preview zeigt den Text genau so, wie er getippt wurde, als Text, und #count zeigt seine Länge von 40, etwa "5 / 40". Ist der Text länger als 40 Zeichen, bekommt #count die Klasse over; ist er wieder kurz genug, verschwindet sie. Der Starter hört auf change, das erst feuert, wenn das Feld den Fokus verliert, und aktualisiert nur die Vorschau.

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: Nichts ändert sich, weil die Tests input-Events senden und der Starter auf change hört.

  2. Hinweis 2

    Hören Sie auf 'input', lesen Sie field.value einmal in eine Konstante und setzen Sie beide textContent-Werte daraus.

  3. Hinweis 3

    Verwenden Sie if...else mit text.length > 40 und count.classList.add('over') oder remove('over').

Eine Lösung zeigen

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

const field = document.querySelector('#status');
const count = document.querySelector('#count');
const preview = document.querySelector('#preview');

field.addEventListener('input', () => {
  const text = field.value;
  preview.textContent = text;
  count.textContent = text.length + ' / 40';
  if (text.length > 40) {
    count.classList.add('over');
  } else {
    count.classList.remove('over');
  }
});
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 field = document.querySelector('#status');
const count = document.querySelector('#count');
const preview = document.querySelector('#preview');

// TODO: update the preview and the count on every input event.
field.addEventListener('change', () => {
  preview.textContent = field.value;
});

main.test.js

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

const type = (page, text) => {
  const field = page.document.querySelector('#status');
  field.value = text;
  field.dispatchEvent(new page.window.Event('input', { bubbles: true }));
};

test('Tippen aktualisiert die Vorschau', async () => {
  const page = await loadPage();
  type(page, 'Hello');
  const text = page.document.querySelector('#preview').textContent;
  assert.equal(text, 'Hello', 'nach dem Tippen von Hello zeigt die Vorschau ' + JSON.stringify(text));
});

test('Der Zähler zeigt die Länge von 40', async () => {
  const page = await loadPage();
  type(page, 'Hello');
  const text = page.document.querySelector('#count').textContent;
  assert.equal(text, '5 / 40', 'nach dem Tippen von Hello zeigt der Zähler ' + JSON.stringify(text));
});

test('Markup im Text erscheint als Text', async () => {
  const page = await loadPage();
  type(page, '<b>hi</b>');
  const preview = page.document.querySelector('#preview');
  assert.equal(preview.children.length, 0, 'die Vorschau hat ' + preview.children.length + ' Element(e) aus dem Text gebaut');
});

test('Text über 40 Zeichen gibt dem Zähler die Klasse over', async () => {
  const page = await loadPage();
  type(page, 'x'.repeat(41));
  const over = page.document.querySelector('#count').classList.contains('over');
  assert.equal(over, true, 'nach 41 Zeichen hat der Zähler keine Klasse over');
});

test('Kürzerer Text entfernt die Klasse over wieder', async () => {
  const page = await loadPage();
  type(page, 'x'.repeat(41));
  type(page, 'ok');
  const over = page.document.querySelector('#count').classList.contains('over');
  assert.equal(over, false, 'nach dem Kürzen hat der Zähler noch die Klasse over');
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Status</title>
</head>
<body>
  <label for="status">Your status</label>
  <input id="status">
  <p id="count">0 / 40</p>
  <p id="preview"></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

Ein Formular, das zu einer Liste hinzufügt

Beim submit soll das Formular den getrimmten Text seines Feldes entry als neues li zu #list hinzufügen und das Feld dann leeren. Ein leerer oder nur aus Leerzeichen bestehender Eintrag fügt nichts hinzu. Der Listener muss event.preventDefault() aufrufen, damit eine echte Seite nicht neu lädt. Escape im Feld leert es; andere Tasten lassen es in Ruhe. Der Starter fügt ungetrimmten Text und Leerzeichen hinzu, leert das Feld nie, bricht den submit nicht ab und ignoriert Escape.

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: " tea " wird mit seinen Leerzeichen hinzugefügt, Leereinträge werden hinzugefügt, und der submit wird nicht abgebrochen.

  2. Hinweis 2

    Geben Sie dem Listener den Parameter event und rufen Sie zuerst event.preventDefault() auf. Trimmen Sie den Wert und kehren Sie früh zurück, wenn er '' ist.

  3. Hinweis 3

    Für Escape: entry.addEventListener('keydown', (event) => { … }) und event.key mit 'Escape' vergleichen.

Eine Lösung zeigen

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

const form = document.querySelector('#add');
const list = document.querySelector('#list');
const entry = form.elements.entry;

form.addEventListener('submit', (event) => {
  event.preventDefault();
  const text = entry.value.trim();
  if (text === '') return;
  const li = document.createElement('li');
  li.textContent = text;
  list.append(li);
  entry.value = '';
});

entry.addEventListener('keydown', (event) => {
  if (event.key === 'Escape') entry.value = '';
});
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 form = document.querySelector('#add');
const list = document.querySelector('#list');

// TODO: cancel the submit, trim, skip blanks, clear the field after adding.
form.addEventListener('submit', () => {
  const li = document.createElement('li');
  li.textContent = form.elements.entry.value;
  list.append(li);
});

// TODO: pressing Escape in the field clears it.

main.test.js

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

const add = (page, text) => {
  page.document.querySelector('#add').elements.entry.value = text;
  page.document.querySelector('#add').requestSubmit();
};
const shown = (page) => [...page.document.querySelectorAll('#list li')].map((li) => li.textContent);
const field = (page) => page.document.querySelector('#add').elements.entry;

test('Abschicken fügt den Text zur Liste hinzu', async () => {
  const page = await loadPage();
  add(page, 'milk');
  assert.deepEqual(shown(page), ['milk'], 'die Liste zeigt ' + JSON.stringify(shown(page)));
});

test('Der Text wird getrimmt und das Feld geleert', async () => {
  const page = await loadPage();
  add(page, '  tea ');
  assert.deepEqual(shown(page), ['tea'], 'die Liste zeigt ' + JSON.stringify(shown(page)));
  assert.equal(field(page).value, '', 'nach dem Hinzufügen enthält das Feld ' + JSON.stringify(field(page).value));
});

test('Ein leerer Eintrag fügt nichts hinzu', async () => {
  const page = await loadPage();
  add(page, '   ');
  assert.deepEqual(shown(page), [], 'die Liste zeigt ' + JSON.stringify(shown(page)));
});

test('Der submit wird mit preventDefault() abgebrochen', async () => {
  const page = await loadPage();
  const form = page.document.querySelector('#add');
  let cancelled = false;
  form.addEventListener('submit', (event) => {
    cancelled = event.defaultPrevented;
  });
  add(page, 'milk');
  assert.equal(cancelled, true, 'das submit-Event wurde nicht abgebrochen');
});

test('Escape leert das Feld', async () => {
  const page = await loadPage();
  field(page).value = 'oops';
  field(page).dispatchEvent(new page.window.KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
  assert.equal(field(page).value, '', 'nach Escape enthält das Feld ' + JSON.stringify(field(page).value));
});

test('Andere Tasten lassen das Feld in Ruhe', async () => {
  const page = await loadPage();
  field(page).value = 'tea';
  field(page).dispatchEvent(new page.window.KeyboardEvent('keydown', { key: 'a', bubbles: true }));
  assert.equal(field(page).value, 'tea', 'nach der Taste a enthält das Feld ' + JSON.stringify(field(page).value));
});

index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Shopping</title>
</head>
<body>
  <form id="add">
    <input name="entry" aria-label="New item">
    <button>Add</button>
  </form>
  <ul id="list"></ul>
  <p>Press Escape to clear the field.</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

Den Listener aufrufen statt ihn zu übergeben

function save(event) {
  console.log('saved by', event.target.id);
}
document.querySelector('#save').addEventListener('click', save());

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'target')

Warum, und die Lösung

save() mit Klammern ruft die Funktion sofort auf, beim Laden der Seite, ohne Event, also ist event undefined. addEventListener soll die Funktion selbst bekommen, um sie später mit dem Event aufzurufen: addEventListener('click', save). Hätte save event nicht benutzt, gäbe es keinen Fehler, und der Klick würde einfach nichts tun. Firefox und Safari formulieren diese Meldung anders.

Ein Feld namens item über form.elements lesen

const form = document.querySelector('#add');
// The field is <input name="item">: read what it starts with.
const start = form.elements.item.value.trim();
console.log(start);

Was Node.js ausgibt

TypeError: Cannot read properties of undefined (reading 'trim')

Warum, und die Lösung

form.elements ist eine Sammlung, und jede Sammlung hat eine Methode item(index). Diese Methode hat Vorrang vor einem Feld namens item, form.elements.item ist also eine Funktion, und ihr value ist undefined. In einem Listener würde li.textContent = form.elements.item.value sogar still scheitern und nichts anzeigen. Nehmen Sie form.elements.namedItem('item') oder geben Sie dem Feld einen anderen Namen, etwa entry.

Einen Listener an eine NodeList hängen

const buttons = document.querySelectorAll('button');
buttons.addEventListener('click', () => console.log('clicked'));

Was Node.js ausgibt

TypeError: buttons.addEventListener is not a function

Warum, und die Lösung

Listener gehören zu einzelnen Elementen; eine NodeList hat kein addEventListener. Fügen Sie jedem Button in einer Schleife einen hinzu: for (const button of buttons) button.addEventListener('click', …). Lektion B6.4 zeigt einen besseren Weg für viele gleichartige Elemente: einen Listener an ihrem gemeinsamen Elternelement.

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

addEventListener statt on…-Attributen

button.addEventListener("click", handler) bittet den Browser, handler bei jedem Klick auf den Button aufzurufen. Übergeben Sie die Funktion selbst, handler, nicht handler(): Die Klammern würden sie einmal sofort aufrufen. Ein Element kann für ein Event mehrere Listener haben, die in der Reihenfolge laufen, in der sie hinzugefügt wurden; dieselbe Funktion wird nicht zweimal hinzugefügt. MDN rät, HTML-Attribute wie onclick="…" nie zu verwenden: Sie mischen JavaScript ins HTML, und die Sicherheitseinstellungen vieler Websites blockieren sie. removeEventListener("click", handler) braucht dieselbe Funktion, geben Sie einem Listener also einen Namen, wenn Sie ihn entfernen wollen. { once: true } entfernt den Listener nach seinem ersten Aufruf.

Das Event-Objekt

Der Browser ruft Ihren Listener mit einem Argument auf, dem Event-Objekt, meist event genannt. event.type ist der Name des Events, etwa "click" oder "submit". event.target ist das Element, auf dem das Event passiert ist. event.currentTarget ist das Element, dessen Listener gerade läuft; beide unterscheiden sich, wenn der Klick auf etwas darin landet, etwa ein Icon in einem Button, was Lektion B6.4 als Bubbling erklärt. Tastatur-Events haben zusätzlich event.key, die gedrückte Taste: "a", "Enter", "Escape". In Tests und eigenen Skripten klickt element.click(), und dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" })) sendet eine Taste.

input und submit

input feuert jedes Mal, wenn der Nutzer den Wert eines Feldes ändert; input.value ist immer ein String, ein Zahlenfeld liefert also "7". Ein Wert, der per Code gesetzt wird, löst kein Event aus. Ein Formular feuert submit, wenn es abgeschickt wird, per Button, per Enter oder mit form.requestSubmit(). Ohne event.preventDefault() würde der Browser das Formular senden und eine neue Seite laden, ein Skript, das das Formular selbst verarbeitet, ruft es also zuerst auf. Lesen Sie Felder über ihren Namen, mit form.elements.entry. Eine Falle: form.elements.item ist die Methode item() der Sammlung, nicht Ihr Feld, .value ergibt also undefined; nennen Sie Felder anders oder nehmen Sie form.elements.namedItem("item").

Quellen

Zuletzt geprüft am 4. Oktober 2026