JavaScript/Tutorials/DOM/Ereignisverarbeitung

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Alle bisherigen Beispiele wurden direkt mit dem Laden des Scripts ausgeführt. Dies hat aber (noch) nichts mit Interaktivität zu tun. Die Event-API ermöglicht die Überwachung und Behandlung von Ereignissen (Event-Handling). Dies können Klicks mit der Maus, Wischgesten (Swipes) auf einem Touchbildschirm, ankommende Daten aus dem Netzwerk oder Änderungen am DOM sein.

Moderne Scripte durchlaufen deshalb verschiedene Phasen:

  1. Das Dokument wird geladen
  2. JavaScript fügt Event-Handler an Elementknoten an.
  3. Der Anwender bedient das Dokument und das Script reagiert darauf.

Event-Handling

Ein HTML-Dokument wird vom Browser geladen, geparst und bleibt dann unverändert im Browser, bis das Fenster geschlossen wird. JavaScript wird zwar ebenso geladen, bietet dann aber eine Interaktivität, mit der der Nutzer das Dokument ändern kann. Dabei werden drei Phasen unterschieden:

  1. Phase Eins: Das Dokument wird empfangen und geparst
    Dabei wird das JavaScript erstmals ausgeführt.
    • Objekte und Funktionen werden dabei definiert, sodass sie für die spätere Nutzung zur Verfügung stehen.
    • Nicht alle notierten Funktionen werden dabei bereits aufgerufen.
    • Zu diesem Zeitpunkt hat das Script noch keinen vollständigen Zugriff auf das Dokument.
  2. Phase Zwei: Das Dokument ist fertig geladen
    Der vollständige Zugriff auf das Dokument über das DOM ist erst jetzt möglich.
    • Registrieren von Event-Handlern: Das Script spricht vorhandene Elementknoten an und fügt ihnen sogenannte Event-Handler mit Handler-Funktionen hinzu.
    • Inhalt oder Darstellung bestehender Elemente können verändert und dem Dokument neue Elemente hinzugefügt werden (siehe DOM-Manipulation).
  3. Phase Drei: Der Anwender bedient das Dokument und das Script reagiert darauf
    Wenn die überwachten Ereignisse an den entsprechenden Elementen im Dokument passieren, werden die entsprechenden Handler-Funktionen ausgeführt.

traditionelles Event-Handling

Es gibt noch viele Script-Beispiele und Tutorials im Netz, in denen Event-Handler als HTML-Eventattribute direkt in das HTML-Dokument geschrieben werden.

obsolete Methode mit HTML-Eventattributen

<button onclick="alert('Hallo Welt!');">Drück mich! </button>

Empfehlung: Eventhandler sollte man in der Regel nicht im HTML selbst notieren, sondern mittels Javascript dynamisch an Elemente binden. Dies folgt der Tugend des unobtrusive JavaScript, denn nur aktiv JavaScript ausführende Browser sollen auch mit JavaScript-Code konfrontiert werden.

Diese Schreibweise mit der Vorsilbe on- wurde auch im traditionellen Event-Handling verwendet. Sie ist Englisch und steht z. B. bei onclick für bei einem Klick.

window.onload

In diesem Beispiel ist der JavaScript-Code im head des Dokuments gespeichert. Der Zugriff auf das output-Element würde erfolgen, bevor der Absatz im DOM überhaupt vorhanden wäre.

Deshalb wird das Script mit window.onload erst ausgeführt, wenn die Seite im Browser geladen und geparst ist.


traditionelles Event-Handling ansehen …
<!DOCTYPE html>
<html lang="de">
<head>
<script>
  window.onload = start;

  function start() {
    document.querySelector('#interaktiv').onclick = klickverarbeitung;
  }

  function klickverarbeitung() {
    document.querySelector('output').innerText += ' Huhu, das ist von Javascript eingefügter Text.';
  }
</script>
</head>
<body>
  <h1>Beispiel für traditionelles Event-Handling</h1>
  <button id="interaktiv">Klick mich!</button>
  <output></output>
</body>
</html>

Da sich der Script-Bereich im head des Dokuments befindet, sind die mit JavaScript angesprochenen Elemente beim Parsen des Skripts noch nicht vorhanden.

Deshalb wird an das globale Window-Objekt mit der Punkt-Notation ein load-Event angehängt, das die Funktion start aufruft.
In der Funktion start erhält der Button, der mit querySelector über seine id interaktiv aufgerufen wird, ein Klick-Event zugewiesen, das onclick (engl. bei einem Klick) die Funktion klickverarbeitung aufruft. Diese weist dem output-Element, das mit querySelector ermittelt wurde, mittels innerText die Zeichenkette zu.


Diese traditionelle Methode scheint sehr einfach, hat jedoch einige Nachteile. So kann man jeweils nur ein Event an Elemente hängen - weitere überschreiben bestehende Event-Handler. Deshalb hat sich eine fortgeschrittene Ereignisvarbeitung mit addEventListener durchgesetzt.

Empfehlung: Vermeiden Sie auf jeden Fall das Hinzufügen von Eventhandlern mit der Punktnotation, wie es in älteren Scripten noch zu sehen ist. Auch wenn es auf den ersten Blick schneller und einfacher scheint, verstößt man so gegen die Trennung von Inhalt und Verhalten und selbst bei kleineren Projekten wird der Code schnell unübersichtlich, wie Sie in diesem Beispiel aus den Jahr 2000 sehen können.

Event-Handler registrieren: addEventListener

Heutzutage hat sich eine einfache Methode, Event-Handler dynamisch an Elemente zu hängen, durchgesetzt:

fortgeschrittenes Event-Handling ansehen …
document.addEventListener('DOMContentLoaded', function () {
 
  document.querySelector('#interaktiv').addEventListener('click', klickverarbeitung);
 
  function klickverarbeitung () {
    document.querySelector('output').innerText += ' Huhu, das ist von Javascript eingefügter Text. \n';
  }
	  
});

Das Beispiel entspricht im HTML-Aufbau und Funktion dem ersten Beispiel.
Da sich der Script-Bereich im head des Dokuments befindet, sind die mit JavaScript angesprochenen Elemente beim Parsen des Skripts noch nicht vorhanden.

Deshalb wird an das document-Objekt mit der addEventListener-Methode ein Event-Handler angehängt, der beim Feuern des DOMContentLoaded-Events eine anonyme Funktion aufruft, die wiederum dem Button mit der id interaktiv einen Event-Handler zuweist, der bei einem Klick die Funktion klickverarbeitung aufruft.


Das DOMContentLoaded-Event bietet gegenüber dem load-Event einige Vorteile, da es schon beim Laden und Parsen des DOM feuert, wenn evtl. umfangreichere externe Skripte und Ressourcen wie Bilder und Videos noch geladen werden.

Beachten Sie: Es gibt für DOMContentLoaded keine entsprechende Objekteigenschaft, das heißt, eine Überwachung dieses Ereignisses muss immer mit addEventListener erfolgen.
Beachten Sie: Eventhandler sind Callbacks. Wenn Sie aus dem Eventhandler heraus auf Variablen zugreifen wollen, die außerhalb der Eventhandler-Funktion definiert sind, beachten Sie bitte die Hinweise zum Umgang mit Callback-Funktionen. Alternativ sollten Sie Bubbling als bessere Alternative in Erwägung ziehen.
Beachten Sie: Die gezeigte Vorgehensweise ist nur dann zielführend, wenn Sie genau wissen, dass Ihr Script ausgeführt wird, während die Eigenschaft readyState des Dokument-Objekts noch auf 'loading' steht. Bei asynchron ausgeführten Scripten, oder solchen, die nachträglich geladen werden, ist das nicht der Fall. Wenn der readyState nicht mehr auf 'loading' steht, dürfen Sie keinen Handler für das DOMContentLoaded Event mehr registrieren, er wird nicht mehr aufgerufen.

Mit diesem fortgeschrittenen Event-Handling könnten an ein Element mehrere Event-Handler angehängt werden, die unterschiedliche Aktionen etwa bei Klick, Touch oder dem Auslösen mit einem Stift auslösen.

Event-Handler entfernen: removeEventListener

Einen mit der addEventListener-Methode hinzugefügten Event-Handler können Sie mit der Schwester-Methode removeEventListener wieder entfernen. Die Methode erwartet dieselben Parameter, die addEventListener beim Registrieren bekommen hat: Einen String mit dem Ereignistyp, die zu löschende Handler-Funktion und schließlich einen optionalen Boolean-Wert für die Event-Phase.

Entfernen eines Event-Handlers mit removeEventListener ansehen …
  document.addEventListener('DOMContentLoaded', function () {
 
    document.querySelector('#interaktiv').addEventListener('click', klickverarbeitung);
    document.querySelector('#entferne').addEventListener('click', entferneKlickverarbeitung);
 
    function klickverarbeitung () {
      document.querySelector('output').innerText += ' Huhu, das ist von Javascript eingefügter Text. \n';
      document.querySelector('#entferne').setAttribute('aria-hidden', false);
    }
	
    function entferneKlickverarbeitung () {
      document.querySelector('#interaktiv').removeEventListener('click', klickverarbeitung);	
      document.querySelector('output').innerText += ' Event-Handler wurde entfernt. \n';
    }
	  
  });

Das Beispiel enthält zwei Buttons, von denen nur der linke sichtbar ist. Der "Entferne!"-Button besitzt ein aria-hidden-Attribut und wird durch CSS ausgeblendet.

Innerhalb der Funktion klickverarbeitung wird der bereits bekannte Text eingefügt. In einer weiteren Anweisung wird das ARIA-Attribut mit setAttribute auf false gesetzt und so durch seine CSS-Festlegung sichtbar.

Durch einen Klick auf den "Entferne"-Button wird die Funktion entferneKlickverarbeitung aufgerufen, die mit removeEventListener('click', klickverarbeitung); die Funktionalität des linken Buttons entfernt und eine entsprechende Meldung im output-Element ausgibt.


Auf Events reagieren

Bis jetzt wurden unsere Programme immer nach dem Laden der Webseite automatisch ausgeführt. Interessant wird es jedoch erst, wenn der Anwender selbständig eingreifen kann und unser Programm reagiert.

Auf Klicks reagieren ansehen …
 <button id="button">Drück mich!</button>
 <output></output>
In diesem Beispiel sehen Sie einen Button (noch ohne Funktion) und das noch leere output-Element.
Das JavaScript befindet sich in einem script-Element im Head des Dokuments:
document.addEventListener('DOMContentLoaded', function () {
 
  let anzahl = 0,
      text   = '';
 
  document.getElementById('button').addEventListener('click', gedrueckt);
 
  function gedrueckt() {
    anzahl = anzahl + 1; 
 
    if (anzahl < 10) {
      text = 'Sie haben den Button ' + anzahl +'mal gedrückt!';
    }
    else if (anzahl < 15) {
      text = 'Sie haben jetzt bereits ' + anzahl + 'mal geklickt.\n' +
             'Wollen Sie sich nicht einen Kaffee gönnen?';
    }
    else {
      text = 'Jetzt haben Sie schon ' + anzahl + 'mal geklickt - mir wird langweilig.';
    }
    document.querySelector('output').innerText = text;
  }
 
});

Wenn das DOM aufgebaut ist, feuert DOMContentLoaded und führt die anonyme Funktion aus.

Mit der addEventListener-Methode wird eine Ereignisüberwachung an den Button gehängt.
Sobald der Button gedrückt (click) wird, wird die Funktion gedrueckt aufgerufen. Diese erhöht den Zähler um 1 und gibt dann aus, wie oft der Button bereits angeklickt wurde.

Die Ereignisüberwachung anderer Events wie mousemove oder touchstart (dem Berühren eines Touchscreens) kann genauso eingerichtet werden.

Event-Objekt

Den Ereignisbehandlungsfunktionen wird ein Argument übergeben: das Ereignisobjekt, das zusätzliche Informationen über das Ereignis enthält. Wenn wir z. B. wissen wollen, welche Maustaste gedrückt wurde, können wir uns die Eigenschaft Event.button des Ereignisobjekts ansehen.

Auf Mausklicks reagieren ansehen …
   let text = '';
 
  document.querySelector('#button').addEventListener('mousedown', event => {
    if (event.button == 0) {
      text = 'Sie haben den linken Mausbutton gedrückt!';
    } else if (event.button == 1) {
      text = 'Sie haben den mittleren Mausbutton gedrückt!';
    } else if (event.button == 2) {
      text = 'Sie haben den rechten Mausbutton gedrückt!';
    }
		document.querySelector('output').innerText = text;
		console.log(text);
  });

Propagation - Weitergabe von Ereignissen

Für die meisten Ereignistypen erhalten Event-Handler, die auf Knoten mit Kindern registriert sind, auch Ereignisse, die in den Kindern auftreten. Wenn ein Button innerhalb eines div-Elements angeklickt wird, sehen die Ereignisbehandler des divs auch das Klick-Ereignis.

Wenn jedoch sowohl das div als auch der Button einen Handler haben, erhält der spezifischere Handler - der auf dem Button - den Vorzug. Man sagt, dass sich das Ereignis nach außen ausbreitet, von dem Knoten, an dem es passiert ist, zu dem übergeordneten Knoten dieses Knotens und weiter bis zum root des Dokuments. Schließlich, nachdem alle Handler, die auf einem bestimmten Knoten registriert sind, an der Reihe waren, erhalten Handler, die auf dem gesamten Fenster registriert sind, die Chance, auf das Ereignis zu reagieren.

Zu jedem beliebigen Zeitpunkt kann ein Event-Handler die Methode stopPropagation auf dem Event-Objekt aufrufen, um zu verhindern, dass weiter oben registrierte Handler das Event erhalten. Dies kann nützlich sein, wenn Sie z. B. einen Button innerhalb eines anderen klickbaren Elements haben und Sie nicht möchten, dass Klicks auf den Button das Klickverhalten des äußeren Elements aktivieren.

Events mit stopPropagation an der Ausbreitung hindern ansehen …
  let div = document.querySelector('div');
  let button = document.querySelector('button');
	
  div.addEventListener('click', () => {
		event.currentTarget.classList.add('chosen');
    console.log('Event-Handler für Div.');
  });
  button.addEventListener('click', event => { 
		event.currentTarget.classList.add('chosen');
    console.log('Event-Handler für Button.');
		if (document.getElementById('check').checked) {
			event.stopPropagation();
		}
  });

Das Beispiel registriert click-Handler sowohl für den Button als auch für das sie umgebende div-Element. Wenn der Button geklickt wird, werden beide Handler ausgeführt. Mit Event.currentTarget wird das Element identifiziert, dessen Handler gerade ausgefüht wird. Mit classList.add erhält es über die Klasse chosen einen farbigen Hintergrund.

In der Funktion für den Button wird überprüft, ob die Checkbox ausgewählt ist. Trifft dies zu, ruft der Handler für den Button stopPropagation auf, wodurch der Handler für das div nicht ausgeführt wird.

Event.target - wo kommt's her?

Die meisten Ereignisobjekte haben eine target-Eigenschaft (engl. für Ziel, hier aber der Auslöser, auf den die Benutzeraktion gerichtet ist), die auf das Element verweist, von dem sie stammen. Sie können diese target-Eigenschaft verwenden, um ein weites Netz für eine bestimmte Art von Ereignis auszuwerfen.

Wenn Sie z. B. ein Formular mit vielen Eingabeelementen haben, kann es bequemer sein, einen einzigen Click-Handler auf dem äußeren Knoten zu registrieren und mit der Target-Eigenschaft herauszufinden, ob und wenn ja welcher Button angeklickt wurde, anstatt einzelne Handler auf allen Schaltflächen zu registrieren.

Events - den Auslöser ermitteln ansehen …
  document.body.addEventListener('click', event => {
    if (event.target.nodeName == 'BUTTON') {
      console.log('geklickt: ', event.target.textContent);
      document.querySelector('output').innerText += event.target.textContent;
    }
  });

In diesem Beispiel gibt es eine virtuelle Tastatur mit 27 Buttons, aber nur einem Event-Handler, der an den body gehängt wurde. In einer if-Abfrage wird mit Event.target ermittelt, ob der Klick von einem Button ausgelöst wurde. Falls dies zutrifft wird der textContent ausgelesen und in der Konsole, bzw. in einem langen String im output-Element ausgegeben.

Empfehlung: Viele Tutorials gehen den Umweg über ein value- oder data-Attribut, um bei einem Button weitere Informationen zu übermitteln. Einfacher und dadurch wartungsärmer ist ein Auslesen des Textinhalts.

Standardverhalten unterdrücken

Die oben erwähnte Einteilung in statisches HTML und dynamisches JavaScript ist so nicht ganz richtig. Es gibt in HTML neben reinen Textauszeichnungelementen wie einer Überschrift oder einem Textabsatz auch Elemente, die bereits eine browsereigene Funktionalität haben, wie z.B. Verweise, Buttons oder input-Elemente und damit verbundene label.

Teilweise ist es jedoch nicht erwünscht, diese Standardaktionen durchzuführen und etliche Eventtypen gestatten es, die Standardaktion zu unterdrücken. Ob ein Eventtyp dies gestattet, erkennen Sie an der cancelable Eigenschaft des Event-Objekts, die Sie mit JavaScript zur Laufzeit abfragen können und die für jeden Eventtyp in der Event-Referenz des Wikis gelistet ist.

Ein click-Event, das z. B. durch Anklicken eines Links ausgelöst wird, würde die aktuelle Seite verlassen. Handelt es sich bei der Seite um ein Formular, könnten bereits Eingaben gemacht worden sein, die damit verloren gehen. Man könnte die Benutzer nun fragen, ob das wirklich gewünscht ist.

Ein submit-Event, das z.B. durch button type="submit" ausgelöst wird, sendet eingegebene Formulardaten ab und sorgt für einen Reload der Seite. Bei einer browsereigenen Validierung sollen richtig eingegebene Daten jedoch erhalten bleiben, sodass das submit-Event unterdrückt werden muss.

Beispiel
document.addEventListener('click', function(event) {
   if (event.target.tagName == "A" && event.target.href) {
      if (!confirm("Ihre eingegebenen Daten gehen verloren. Sind Sie sicher?"))
         event.preventDefault();
   }
});
click Events unterliegen dem bubbling und werden deshalb auch am Dokument sichtbar. Die target Eigenschaft des Event-Objekts verweist auf das angeklickte Element. Ist dies ein a Element mit einer href-Eigenschaft, und wurden Formulardaten verändert (was hier durch eine globale Variable angedeutet wird), wird in einer Bestätigungsbox abgefragt, ob der Nutzer die Seite wirklich verlassen möchte. Wenn nicht, wird der Klick durch Aufruf der preventDefault-Methode des Event-Objekts unwirksam gemacht. Außerdem wäre zu überlegen, ob man bei leeren oder (unberührten) Eingabefeldern die Abfrage weglassen sollte.
Beachten Sie: Versuchen Sie nicht, mit Hilfe der unload oder beforeunload Events das Verlassen der Seite zu unterdrücken. Bei unload wird Ihnen das auf keinen Fall gelingen, weil dieses Event nicht cancelable ist. Bei beforeunload zeigen nur manche Browser eine Dialogbox an (z.B. Firefox), andere (z.B. Chrome) verweigern die Blockade des unload-Vorgangs durch einen Dialog, weil sich das zum „Einsperren“ eines Anwenders auf der Seite missbrauchen lässt. Bestätigungsabfragen sollten Sie daher stellen, bevor das Verlassen der Seite ausgelöst wird, also im submit Event eines Forms oder im click Event eines Buttons oder Links

Mit preventDefault können Sie bei einem Event, das cancelable ist, die Standardaktion des Browsers unterdrücken, wie z. B. in diesem Beispiel, in dem die verlinkte Großansicht eines Bildes schon innerhalb der Webseite angezeigt wird. Der Klick auf den Link löst das JavaScript, aber kein Navigieren zu einer neuen Ressource aus.

Bubbling, Capturing und stopPropagation

Bei ineinander verschachtelten Elementen feuern Events nicht nur beim auslösenden Element, sondern auch bei dessen Elternelementen. Die Phase, in der das Event zu den Elternelementen aufsteigt, nennt man Bubbling-Phase, die Phase, in der das Event absteigt, nennt man Capturing-Phase. Die Methode addEventListener hat noch einen dritten optionalen Parameter, useCapture, über den gesteuert wird, in welcher Phase der Eventhandler aufgerufen wird. Mit der Eigenschaft des Eventobjekts stopPropagation() können Bubbling bzw. Capturing verhindert werden.

Mit dieser Testseite soll das Event-Capturing und -Bubbling veranschaulicht werden. Dazu wurden mehrere Divs mit farbigem Hintergrund ineinander verschachtelt.

Für jedes dieser Divs kann ein Eventhandler für pointerup, pointerdown, keyup und keydown registriert werden. Mit useCapture true werden die Eventhandler in der Capturing-Phase ausgeführt, mit useCapture false in der Bubbling-Phase.

Die Eventhandler schreiben einen Eintrag in die Browserkonsole und lassen für eine kurze Zeit eine Textinfo erscheinen. Die Info erscheint zeitverzögert, um die Abfolge auch verfolgen zu können.

Das hellblaue Div ist absolut positioniert und verschoben.

Bitte mit Maus, Finger oder Stift in eines der farbigen Rechtecke klicken, oder die Divs mit der Tabulatortaste anwählen und dann eine Taste drücken.

Eventphasen zeigen und Events mit stopPropagation an der Ausbreitung hindern ansehen …
// Eventhandler nach Wunsch registrieren
if(document.querySelector("#downFalse").checked) {
	eventElement.addEventListener("pointerdown", handleEventUseCaptureFalse, false);
	eventElement.addEventListener("keydown", handleEventUseCaptureFalse, false);
}
if(document.querySelector("#downTrue").checked) {
	eventElement.addEventListener("pointerdown", handleEventUseCaptureTrue, true);
	eventElement.addEventListener("keydown", handleEventUseCaptureTrue, true);
}
if(document.querySelector("#upFalse").checked) {
	eventElement.addEventListener("pointerup", handleEventUseCaptureFalse, false);
	eventElement.addEventListener("keyup", handleEventUseCaptureFalse, false);
}
if(document.querySelector("#upTrue").checked) {
	eventElement.addEventListener("pointerup", handleEventUseCaptureTrue, true);
	eventElement.addEventListener("keyup", handleEventUseCaptureTrue, true);
}

// Die Eventhandler
function handleEventUseCaptureTrue(event) {
	eventHandler(this, event, true);
}

function handleEventUseCaptureFalse(event) {
	eventHandler(this, event, false);
}

function eventHandler(eventElement,event,useCapture) {
	// Wenn Checkbox für Stoppropagation angehakt, Event nicht weiterreichen, kein bubbling und kein capturing
	if(chkStopPropagationElement.checked) event.stopPropagation();

	// Auf diese Tastaturereignisse soll nicht reagiert werden
	if(event.key == "Tab" || event.key == "Shift" || event.key == "Alt" || event.repeat) return;

	// Die drei Eventphasen
	let phase = event.eventPhase;
	if(event.eventPhase == 1) phase = "Capturing-Phase";
	else if(event.eventPhase == 2) phase = "Zielphase";
	else if(event.eventPhase == 3) phase = "Bubbling-Phase";

	// Konsolenausgabe
	console.log(event.type,eventElement.id,phase,"useCapture: "+useCapture);

	// Events sichtbar machen
	visualizeEvents.add(eventElement,event,phase,useCapture);
}

Auf dieser Testseite werden Divs als anklickbare Elemente verwendet. Dadurch ist die Seite nicht zugänglich. Das ist aber auf einer Testseite ok.


Siehe auch

JavaScript/Tutorials