Tutorials › Teil 3: Die Seite

Kapitel 13 von 16

Auf Ereignisse reagieren

<button id="knopf">Drück mich</button>
<p id="zaehler">Noch nichts.</p>
<script>
  let klicks = 0;
  const anzeige = document.getElementById("zaehler");

  document.getElementById("knopf").addEventListener("click", () => {
    klicks++;
    anzeige.textContent = klicks === 1 ? "Einmal." : klicks + " Mal.";
  });
</script>

Drei Bestandteile: ein Element, ein Ereignisname, eine Funktion. Der Browser merkt sich die Funktion und ruft sie auf, wenn das Ereignis eintritt.

Warum nicht onclick

In den Skripten dieses Archivs steht das Gegenstück fast immer im HTML:

<input type="button" value="Übersetzen!" onclick="javascript:translate(this.form)">

Das funktioniert. Es hat nur vier Nachteile, die sich mit der Zeit summieren.

Es geht genau ein Zuhörer pro Ereignis. Ein zweites onclick überschreibt das erste stillschweigend.

Der Code steht in einem Attribut, also als Zeichenkette. Kein Editor prüft ihn, kein Werkzeug findet ihn, Syntaxfehler fallen erst beim Klicken auf.

Die aufgerufene Funktion muss global sein. In einem Modul aus Kapitel 2 existiert sie das gar nicht.

Und Verhalten steht im Markup, wo Struktur stehen sollte. Wer das HTML umbaut, verliert versehentlich Funktionalität.

Das javascript: vorne ist übrigens überflüssig, es hat in einem Attribut nie eine Bedeutung gehabt.

Mehrere Zuhörer

<button id="k">Zweimal zuhören</button>
<script>
  const k = document.getElementById("k");

  k.addEventListener("click", () => zeige("Erster Zuhörer"));
  k.addEventListener("click", () => zeige("Zweiter Zuhörer"));
</script>

Beide laufen, in der Reihenfolge der Anmeldung. Das erlaubt es, unabhängige Bausteine auf dasselbe Ereignis reagieren zu lassen, ohne dass einer vom anderen wissen muss.

Abmelden geht nur mit einer benannten Funktion:

<button id="k">Funktioniert genau einmal</button>
<script>
  const k = document.getElementById("k");

  function reagieren() {
    zeige("Hier bin ich, und jetzt gehe ich.");
    k.removeEventListener("click", reagieren);
  }

  k.addEventListener("click", reagieren);
</script>

Für genau diesen Fall gibt es eine Abkürzung:

<button id="k">Auch nur einmal</button>
<script>
  document.getElementById("k").addEventListener("click", () => {
    zeige("Einmalig.");
  }, { once: true });
</script>

Das Ereignisobjekt

Die Zuhörerfunktion bekommt ein Argument. Darin steht alles über das, was gerade passiert ist.

<div id="feld" style="height:90px;background:#eee;border:1px solid #ccc">
  Klick irgendwohin.
</div>
<script>
  document.getElementById("feld").addEventListener("click", (e) => {
    zeige("Typ:", e.type);
    zeige("Position im Element:", Math.round(e.offsetX), "/", Math.round(e.offsetY));
    zeige("Ausgelöst durch:", e.target.tagName);
  });
</script>

e.target ist das Element, auf dem das Ereignis tatsächlich ausgelöst wurde. Das ist nicht zwangsläufig das Element, an dem der Zuhörer hängt, und dieser Unterschied ist gleich noch wichtig.

Der Parameter heißt oft e oder event. Der Name ist frei wählbar.

Die Vorgabe abschalten

Manche Ereignisse haben eine eingebaute Reaktion. Ein Link springt zur Zieladresse, ein Formular wird abgeschickt, ein Rechtsklick öffnet ein Menü.

<a href="https://example.com" id="link">Dieser Link führt nirgendwohin</a>
<script>
  document.getElementById("link").addEventListener("click", (e) => {
    e.preventDefault();
    zeige("Der Sprung wurde abgefangen.");
  });
</script>

preventDefault unterdrückt nur die eingebaute Reaktion. Der Zuhörer läuft trotzdem, und andere Zuhörer auch.

Tastatur

<input id="feld" placeholder="Tipp etwas" style="padding:6px;width:220px">
<script>
  document.getElementById("feld").addEventListener("keydown", (e) => {
    zeige("Taste:", e.key, "| Code:", e.code);
    if (e.key === "Enter") zeige("Abgeschickt.");
    if (e.key === "Escape") { e.target.value = ""; zeige("Geleert."); }
  });
</script>

e.key ist das Zeichen oder der Name der Taste, also "a", "Enter", "ArrowLeft". Das ist fast immer das, was du brauchst.

e.code beschreibt die physische Position auf der Tastatur, unabhängig vom Layout. Für Spielsteuerungen ist das besser, weil KeyW auch auf einer französischen Tastatur an derselben Stelle liegt.

Das alte e.keyCode mit seinen Zahlen ist veraltet. Wenn du in einem Archivskript if (e.keyCode == 37) liest: 37 ist Pfeil links.

Delegation

Zwanzig Knöpfe, zwanzig Zuhörer? Muss nicht sein.

<div id="tastatur">
  <button data-wert="7">7</button>
  <button data-wert="8">8</button>
  <button data-wert="9">9</button>
  <button data-wert="C">C</button>
</div>
<p id="anzeige">0</p>
<script>
  const anzeige = document.getElementById("anzeige");

  document.getElementById("tastatur").addEventListener("click", (e) => {
    const knopf = e.target.closest("button");
    if (!knopf) return;

    const wert = knopf.dataset.wert;
    anzeige.textContent = wert === "C" ? "0" : anzeige.textContent.replace(/^0$/, "") + wert;
  });
</script>

Der Zuhörer hängt am umschließenden div, nicht an den Knöpfen. Ereignisse steigen im Dokument nach oben, deshalb kommt jeder Klick dort an.

e.target sagt, wo es losging. closest("button") sucht von dort aufwärts das nächste passende Element, damit auch ein Klick auf ein Symbol innerhalb des Knopfes richtig zugeordnet wird.

Der große Vorteil: Knöpfe, die erst später per Skript dazukommen, funktionieren sofort mit. Bei einzeln angemeldeten Zuhörern müsste man daran denken, sie nachzurüsten.

Ereignisse, die man oft braucht

<input id="a" placeholder="input feuert bei jedem Zeichen" style="width:260px;padding:5px">
<br><br>
<select id="b"><option>eins</option><option>zwei</option></select>
<script>
  document.getElementById("a").addEventListener("input", (e) => {
    zeige("input:", e.target.value.length, "Zeichen");
  });
  document.getElementById("b").addEventListener("change", (e) => {
    zeige("change:", e.target.value);
  });
</script>

input feuert bei jeder Änderung, change erst wenn der Wert feststeht. Bei Textfeldern willst du meistens input, bei Auswahllisten change.

Weitere häufige: submit für Formulare, mouseenter und mouseleave für Hover-Verhalten, scroll am Fenster, DOMContentLoaded wenn das Dokument steht.

Zu viele Ereignisse

scroll und input können hundertfach pro Sekunde feuern. Steckt in der Reaktion etwas Aufwendiges, ruckelt die Seite.

<input id="suche" placeholder="Tipp schnell" style="width:260px;padding:5px">
<script>
  let wartet;

  document.getElementById("suche").addEventListener("input", (e) => {
    const wert = e.target.value;
    clearTimeout(wartet);
    wartet = setTimeout(() => zeige("Jetzt wird gesucht nach:", wert), 400);
  }, false);
</script>

Jeder Tastendruck verwirft den vorherigen Auftrag und setzt einen neuen. Erst wenn vierhundert Millisekunden Ruhe herrschen, passiert etwas. Das Verfahren heißt Entprellen und steckt auch im Probierfeld dieses Kurses.

Ein Reaktionstest

Alles aus diesem Kapitel in einem kleinen Programm. Das Original liegt unter Reaktionstest.

<div id="feld" style="padding:24px;text-align:center;background:#ddd;
     border-radius:8px;cursor:pointer;user-select:none">
  Klick, um zu starten
</div>
<script>
  const feld = document.getElementById("feld");
  let start = 0;
  let zustand = "bereit";

  feld.addEventListener("click", () => {
    if (zustand === "bereit") {
      zustand = "wartet";
      feld.textContent = "Warte auf Grün...";
      feld.style.background = "#c0392b";
      const verzoegerung = 800 + Math.random() * 2500;

      setTimeout(() => {
        zustand = "los";
        feld.style.background = "#27ae60";
        feld.textContent = "JETZT!";
        start = performance.now();
      }, verzoegerung);
      return;
    }

    if (zustand === "wartet") {
      zustand = "bereit";
      feld.style.background = "#ddd";
      feld.textContent = "Zu früh. Nochmal klicken.";
      return;
    }

    const zeit = Math.round(performance.now() - start);
    zustand = "bereit";
    feld.style.background = "#ddd";
    feld.textContent = zeit + " ms. Nochmal?";
  });
</script>

Die Variable zustand ist das Entscheidende. Ein Klick bedeutet je nach Zustand etwas anderes, und ohne diese Unterscheidung würde ein hektischer Klicker das Programm durcheinanderbringen.

Formulare sind der nächste Schritt, und dort gibt es mehr abzufangen als einen Klick.