Javascript Event Delegation – ein EventListener für viele (Formular-)Elemente
Statt mehrere eventListener für eine Gruppe von Elementen einzusetzen, beobachtet ein eventListener ein umfassendes Element als »Delegierten«. Das ist eine elegante Technik, die auf dem Event Bubbling basiert und die viele der typischen Formular-Abhängigkeiten vermeidet.
Event Delegation
Bei der Event Delegation wird einer Gruppe von Elementen ein »Abgesandter« zugewiesen, der diese Elemente auf die gleiche Art behandeln kann. Das ist eine typische Situation bei vielen Formularen: Erst wenn alle Felder ausgefüllt sind, startet die Berechnung oder Datenübertragung.
b12.addEventListener('input', calcB6);
b13.addEventListener('input', calcB6);
b4.addEventListener('input', calcTotal);
b5.addEventListener('input', calcTotal);
Das wäre keine schöne Herangehensweise: viele EventListener, das Script wäre schwer wartbar und die Logik wild verteilt.
<form id="calculator"> <input name="b4"> <input name="b5"> <input name="b12"> <input name="b13"> <output name="b6"></output> <output name="total"></output> </form>
Event Delegation macht sich das Event Bubbling zunutze: Events steigen von einem Element in der Bubbling-Phase über alle übergeordneten Elemente bis zum root-Element auf.
Bei einer langen Liste von Elementen, die bei einem Event in ähnlicher Weise behandelt werden, muss nicht jedes Event in einem eigenen Skript behandelt werden. Statt dessen beobachtet der eventListener ein im DOM-Baum übergeordnetes Element, das alle Elemente der Liste enthält.
const form = document.querySelector('#calculator');
form.addEventListener('input', handleFormInput);
Hier laufen alle Eingaben zusammen.
function handleFormInput(event) {
const data = getFormValues(event.currentTarget);
updateB6(data);
updateTotal(data);
}
function getFormValues(form) {
return {
b4: Number(form.b4.value) || 0,
b5: Number(form.b5.value) || 0,
b12: Number(form.b12.value) || 0,
b13: Number(form.b13.value) || 0
};
}
Jedes Eingabefeld kann jederzeit geändert werden, weil getFormValues den Zustand der Eingabefelder »beobachtet«. Wenn alle vier Eingabefelder ausgefüllt sind, kann die Berechnung erfolgen. Wenn b12 und b14 ausgefüllt sind, kann Ausgabefeld b6 berechnet werden.
function updateB6({ b12, b13 }) {
const output = document.querySelector('[name="b6"]');
if (b12 > 0 && b13 > 0) {
output.value = round2(b12 * b13);
} else {
output.value = '';
}
}
function updateTotal({ b4, b5, b12, b13 }) {
const output = document.querySelector('[name="total"]');
if (b4 > 0 && b5 > 0 && b12 > 0 && b13 > 0) {
output.value = round2(b4 + b5 + b12 + b13);
} else {
output.value = '';
}
}
Das Formular kommt mit nur einem eventListener aus. Selbst wenn ein weiteres Feld hinzukommt, fällt kein Umbau des Scripts an. In welcher Reihenfolge der Benutzer die Eingabefelder ausfüllt, spielt keine Rolle. Das Formular mit einem eventListener als Delegierten ist eine Zustandsmaschine und keine Kettenreaktion auf Events.
Dieser Ablauf passt nicht nur auf einfache Berechnungen, sondern auch auf komplexe Formulare mit und ohne Abhängigkeiten.
Mehrere Events für ein Element
Um mehrere Event-Typen für ein Element zu registrieren, braucht das Skript eine Schleife oder eine Funktion.
.container {
position: relative;
overflow: hidden;
}
#zoomable-image {
height: 100%;
transition: transform 0.8s ease;
}
#zoomable-image.zoomed {
transform: scale(2);
}
const image = document.getElementById("zoomable-image");
const events = ["click", "touchstart"];
function addEventListeners(element, events, handler) {
events.forEach(event => {
element.addEventListener(event, handler);
});
}
function toggleZoom() {
this.classList.toggle("zoomed");
}
addEventListeners(image, events, toggleZoom);
Das Array events enthält die Events, die registriert werden sollen: in diesem Fall click und touchstart.
const image = document.getElementById("zoomable-image");
const events = ["click", "touchstart"];
function addEventListeners(element, events, handler) {
events.forEach(event => {
element.addEventListener(event, handler);
});
}
addEventListeners(image, events, function (evt) {
evt.preventDefault();
this.classList.toggle("zoomed");
});
Am Rande: evt.preventDefault() wird eingesetzt, damit das Bild auf dem Touchscreen nicht Fullscreen angezeigt wird.
stopPropagation
Es gibt aber Situationen, in denen die Erkennung eines Events schon in der Capture-Phase eine wichtige Rolle spielt. Auf dem Weg nach unten kann stopPropagation das Event unterbinden, so dass es nicht bis zum Ziel – zum Event Target – vordringt.
let b1 = document.querySelector("#b1");
b1.addEventListener ("click", function (){
b1.classList.toggle ("red");
}
);
let stopit = document.querySelector("#buttons");
stopit.addEventListener("click", function (eve) {
eve.stopPropagation ();
}, true)
Die Weiterleitung des Events wird unterbunden, das Event kommt niemals bei seinem eigentlichen Event Target an. Zu einer Bubble-Phase kommt es auch nicht mehr. Finito.
Oft werden sowohl bubble als auch capture ausgeschaltet, damit es nicht zu unvorhergesehenen Aktionen kommt. Dafür gibt es das stopPropagation() sowohl für die modernen Browser als auch für IE.
- DOM: Event stopPropagation()
- Stopt den Ereignis-Fluss. Kann entweder in der Capture- oder in der Bubble-Phase benutzt werden.
- IE: stopPropagation()
- In IE ist stopPropagation() keine Methode, sondern eine Boole'sche Eigenschaft, die auf true gesetzt wird, damit das Ereignis nicht aufsteigt (bubbling)
Auch wenn wir bubble in den meisten Fällen gern abschalten würden, kann das zu unvorhergesehenen Fehlern führen. Vielleicht hängt ein Plugin vom Event ab …?