Manche JavaScript-Applikationen haben eine erhebliche Zahl an Event-Listenern. Diese ordentlich und übersichtlich zu erfassen und abzuarbeiten kann eine Herausforderung sein – bis wir uns die Anleitung zu addEventListener genau durchlesen.

Wie schon in dem Artikel „Event-Handling mit JavaScript – und ohne jQuery“ angemerkt, ist ein gutes Verständnis von addEventListener sehr hilfreich, um sich in der eigenen JavaScript-Applikation keine Performance-Ärger einzubrocken – und gleichzeitig die Übersicht zu behalten, wenn die Anzahl der Listener schnell ansteigt.

Gehen wir davon aus, dass ihr klassen- bzw. objektorientiert arbeitet. Schnell werdet ihr auf die Herausforderung treffen, dass this innerhalb des Listeners eine andere Bedeutung hat als außerhalb.

Natürlich könntet ihr mit .bind() den Kontext neu setzen. Oder ihr könntet mit anonymen Arrow-Funktionen arbeiten, die aber den üblichen Nachteil von anonymen Funktionen bei Listenern mitbringen.

MDN's Anleitung für addEventListener verrät uns, dass als Listener drei verschiedene Dinge eingetragen werden können:

  1. null
  2. Eine Funktion, ggf. auch eine anonyme Funktion
  3. Ein Objekt, wenn es über eine handleEvent-Methode verfügt.

Die letzte Option erlaubt es uns, this mit wenig Aufwand vorhersehbar einzusetzen.

Objekte als Event-Listener

Tatsächlich ist die Übergabe des gesamten Objekts als Listener und die Definition einer handleEvent-Methode überraschend simpel:

// 1st example: Multiple event listeners attached,
//              one handler
class App {
  constructor() {
    document.querySelectorAll('a, button').forEach(function(element) {
      element.addEventListener('click', this);
    });
  }

  handleEvent(event) {
    // do something
  }
}

Innerhalb von handleEvent kann nun this mit dem zu erwartenden Bezug auf das aktuelle Klassen-Objekt verwendet werden, während die für das Event notwendigen Eigenschaften (ebenfalls regulär) in dem Parameter event stehen.

Ärgerlicherweise werden nun aber alle so registrierten Events mit ein und dem selben Listener bedient. Zum Glück verrät uns aber jedes Event, für welches Element es ausgelöst wurde – die sogenannte Event-Delegation. Eine Fallunterscheidung z.B. nach ID kann uns also helfen, die einzelnen Aktionen auseinander zu halten:

// 2nd example: Multiple event listeners attached,
//              one handler with delegation per id
class App {
  constructor() {
    document.querySelectorAll('a, button').forEach(function(element) {
      element.addEventListener('click', this);
    });
  }

  handleEvent(event) {
    switch (event.target.id) {
      case 'example-1':
        // do something
        break;
      case 'example-2':
        // do something other
        break;
    }
  }
}

…und aus dem Nachteil ist ein Vorteil geworden, da jetzt die einzelnen Event-Listenern nichts anderes als Fälle in einer switch-Anweisung geworden sind.

Jetzt können wir die Vielzahl der im DOM verteilten Event-Listener eigentlich auch gleich auf einen einzigen reduzieren, da der Listener sowieso nochmals überprüft, welches Element genau getroffen wurde. Dabei machen wir uns Event-Bubbling zu Nutze, und montieren an zentraler Stelle einen Event-Listener, der alle Interaktionen abfängt, und dann mittels Event-Delegation den korrekten Ansprechpartner auswählt.

Die zentralste Stelle ist natürlich… der <body>. Damit erschlagen wir übrigens gleich den Fall, dass nachträglich dynamisch DOM-Elemente auf der Seite hinzugefügt werden. Denn der auf dem <body> montierte Listener fängt natürlich auch erst nachträglich im DOM montierte Elemente ab.

// 3rd example: Single event listeners attached,
//              one handler with delegation per id
class App {
  constructor() {
    document.body.addEventListener("click", this);
  }

  handleEvent(event) {
    switch (event.target.id) {
      case 'example-1':
        // do something
        break;
      case 'example-2':
        // do something
        break;
    }
  }
}

Noch weiter abstrahieren lässt sich das Delegations-Beispiel, wenn wir vorher die unterschiedlichen Event-Typen auseinandersortieren:

// 4th example: Single event listeners attached,
//              one handler triggers subhandlers per event type
class App {
  constructor() {
    document.body.addEventListener("input", this, true);
    document.body.addEventListener("click", this, true);
    document.body.addEventListener("change", this, true);
  }

  handleEvent(event) {
    // @see https://developer.mozilla.org/en-US/docs/Web/API/Event
    switch (event.type) {
      case 'click' : this.handleEventClick(event);  break;
      case 'input' : this.handleEventInput(event);  break;
      case 'change': this.handleEventChange(event); break;
    }
  }

  handleEventClick(event) {
    switch (event.target.id) {
      case 'example-1':
        // do something
        break;
      case 'example-2':
        // do something
        break;
    }
  }

  handleEventInput(event) {
    switch (event.target.id) {
      case 'example-3':
        // do something
        break;
      case 'example-4':
        // do something
        break;
    }
  }

  handleEventChange(event) {
    switch (event.target.id) {
      case 'example-5':
        // do something
        break;
      case 'example-6':
        // do something
        break;
    }
  }
}

Dieses Pattern lässt sich natürlich auch ohne Objekte einsetzen, und mit regulären Funktionsaufrufen durchführen. Dabei verliert es aber natürlich die Möglichkeit, this ohne Gehirn- oder Code-Akrobatik zu verwenden.

Deklaration der Event-Handler im HTML

Unser schönes Beispiel geht davon aus, dass wir die Auswahl der eigentlich durchzuführenden Aktion an der id des Elements festmachen, dass das Event ausgelöst hat. Tatsächlich können wir das aber an jedes Attribut koppeln. Damit sind auch class-Attribute mögliche Ziele.

<dialog>
  <button class="modal-close">Close<button>
  
  <label for="date">Input</label>
  <input id="date" type="date" class="change-date" />

  <button class="set-to-current">Set to current time</button>
</dialog>

Das erlaubt uns zum Beispiel, die erste Klasse eines Elements darüber entscheiden zu lassen, was getan werden soll:

// 5th example: Single event listeners attached,
//              one handler triggers subhandlers per `class` attribute
class App {
  constructor() {
    document.body.addEventListener("input", this, true);
    document.body.addEventListener("click", this, true);
    document.body.addEventListener("change", this, true);
  }

  handleEvent(event) {
    // Get first `class` of element
    const handler = e.target.classList.item(0);
    switch (handler) {
      case 'modal-close'   : this.handleModalClose(event);   break;
      case 'change-data'   : this.handleChangeDate(event);   break;
      case 'set-to-current': this.handleSetToCurrent(event); break;
    }
  }

  handleModalClose(event) {
    // do something
  }

  handleChangeDate(event) {
    // do something
  }

  handleSetToCurrent(event) {
    // do something
  }
}

Aber wie wäre es mit einem eigenen data-Attribut, zum Beispiel in Form eines data-handler?

<dialog>
  <button data-handler="modal-close">Close<button>
  
  <label for="date">Input</label>
  <input id="date" type=="date" data-handler="change-date" />

  <button data-handler="set-to-current">Set to current time</button>
</dialog>

In diesem Fall hängen wir an alle HTML-Elemente ein data-handle, mit dem wir zum Beispiel auch direkt benennen können, mit welchem Handler gearbeitet werden soll:

// 5th example: Single event listeners attached,
//              one handler triggers subhandlers per `data-handler` attribute
class App {
  constructor() {
    document.body.addEventListener("input", this, true);
    document.body.addEventListener("click", this, true);
    document.body.addEventListener("change", this, true);
  }

  handleEvent(event) {
    // Bubble up to closest DOM element with `data-handler`
    const handler = e.target.closest('[data-handler]')?.dataset.handler;
    switch (handler) {
      case 'modal-close'   : this.handleModalClose(event);   break;
      case 'change-data'   : this.handleChangeDate(event);   break;
      case 'set-to-current': this.handleSetToCurrent(event); break;
    }
  }

  handleModalClose(event) {
    // do something
  }

  handleChangeDate(event) {
    // do something
  }

  handleSetToCurrent(event) {
    // do something
  }
}

Als angenehmer Nebeneffekt können wir uns auf die Elemente beschränken, die auch wirklich ein data-handler-Attribut haben – oder im DOM nach oben wandern, bis wir ein Element gefunden haben, dass über ein solches Attribut verfügt. .closest() ist hier eine wirkliche Hilfe.


Andere Artikel zum Thema · · ·

Zuletzt geändert am

fboës - Der Blog