EventListener schachteln für click/keyup-Behandlung

- javascript
0 Nico R.
0 Felix Riesterer
0 Nico R.
Hallo zusammen,
ich komme gerade nicht weiter. Das Dialogfenster im Beispielscript kann man per Mausklick auf OK oder per Enter-Taste schließen (und dabei Daten absenden), siehe Konsolenausgabe. Nutze ich nur den Mausklick oder nur Enter, ist alles in Ordnung. Wenn ich aber z.B. den dialog beim ersten Mal per Enter schließe und beim zweiten Mal per click, wird der click zweimal ausgeführt.
button.addEventListener("click", function(event){
dialog.showModal();
dialog_ok.addEventListener("click", function(event) {
console.log("dialog klick OK");
dialog.close();
}, {once: true} );
dialog.addEventListener("keydown", function(event) {
console.log("dialog keydown");
if(event.keyCode == 13) {
console.log("dialog keydown OK");
event.preventDefault();
dialog.close();
}
}, {once: true} );
});
Bei anderen Kombinationen wirds richtig wild... Das Ganze überschlägt und summiert sich vermutlich, weil ich mit jedem Aufruf des dialog-Fensters per button auch die Event-Handler für click und clack mehrfach registriere. Das hatte ich versucht über die Option once zu verhindern, aber das hatte nicht den erhofften Effekt.
Am einfachsten lässt sich das Problem im Beispielscript beheben, indem ich die beiden inneren EventListener außerhalb des button-EventListeners packe.
Aber gibt es auch eine Lösung, ohne die beiden auszulagern?
Schöne Grüße
Nico
Wie das so ist... Wenn man das Problem aufgeschrieben hat, kommt einem manchmal die Lösung. Diese hier - per Schalter - ist nicht gerade elegant, hilft mir aber erstmal:
let erstaufruf = true;
button.addEventListener("click", function(event){
dialog.showModal();
if(erstaufruf) {
dialog_ok.addEventListener("click", function(event) {
console.log("dialog klick OK");
dialog.close();
});
}
if(erstaufruf) {
dialog.addEventListener("keydown", function(event) {
console.log("dialog keydown");
if(event.keyCode == 13) {
console.log("dialog keydown OK");
event.preventDefault();
dialog.close();
}
});
}
erstaufruf = false;
});
Gibts das auch in schöner?
Schöne Grüße
Nico
@@Nico R.
Gibts das auch in schöner?
Ich denke, schon.
Man kann Eventlistener auch wieder entfernen.
Aber warum registstrierst du die überhaupt im Eventhandler? Wenn du die da rausnimmst, sollte sich das Problem erledigt haben.
Ach, und noch was:
if(event.keyCode == 13) {
Ist 13 deine Glückszahl oder was hat die zu bedeuten? Das ist unleserlicher Code. Außerdem ist keyCode
veraltet.
Jolan tru
Hallo Gunnar,
Man kann Eventlistener auch wieder entfernen.
Hm, ja. Entfernen will ich sie ja nicht, sondern nur einmalig registrieren.
Aber warum registstrierst du die überhaupt im Eventhandler? Wenn du die da rausnimmst, sollte sich das Problem erledigt haben.
Das ist in dem Script damals so entstanden. Ich müsste, wenn ich die beiden Funktionen auslagere, noch mehr auslagern bzw. stehen dann bestimmte Parameter nicht mehr zur Verfügung usw. Es wäre ein größerer Umbau, da ist die Lösung mit dem Schalter jetzt das einfachste.
Ist 13 deine Glückszahl oder was hat die zu bedeuten? Das ist unleserlicher Code. Außerdem ist
keyCode
veraltet.
Wieso unleserlich? Die 13 war der keyCode für Enter. Hat bis dato auch funktioniert. Ich habs aber jetzt auf das einfachere und verständlichere key geändert. Danke für den Hinweis.
Schöne Grüße
Nico
Hallo Nico R.,
bzw. stehen dann bestimmte Parameter nicht mehr zur Verfügung
Was soll da denn fehlen? Im Moment stehen sie innerhalb des click-Handlers vom Button. Der kriegt eh keine Parameter. Oder generierst Du in diesem Handler Daten, die die Funktionen beim Auslösen des close benötigen?
Ist der reale Button irgendwo auf der Homepage in Gebrauch, dass man sich das angucken könnte?
Rolf
Hallo Rolf,
nee, das ist das Redaktionssystem. Schwer zu erklären. Innerhalb dieser anonymen Funktion passiert noch viel mehr. Es gibt dort auch wiederum Funktionsdeklarationen usw.
Ich denke/dachte mir, dass es relativ egal ist, ob ich eine Funktion mit function funktionsname() { } deklariere und dann mit addEventlistener("click", funktionsname) aufrufe oder ob ich direkt die anonyme Funktion als Kapselung nutze. Bisher kam ich damit gut zurecht, das hab ich mir irgendwie so angewöhnt.
Im Zuge dieses Konstrukts bin ich aber gerade auf ein neues Problem gestoßen, hier im neuen Beispielscript zu sehen. Sagen wir mal, es gibt jetzt innerhalb der anonymen Funktion eine globale Variable val, die aus einem input-Feld außerhalb des dialogs kommt. Beim ersten Absenden (Nummer eingeben => dialog öffnen => OK) wird val korrekt ausgegeben, bei allen folgenden ändert es aber den Wert nicht mehr:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
</head>
<body>
<input type="number" min="1" max="10">
<button id="button">Dialog öffnen</button>
<dialog id="dialog">
<button id="dialog_ok" type="button">OK</button>
<button id="dialog_abbr" type="reset">Abbrechen</button>
</dialog>
<script>
(function () {
const button = document.getElementById("button");
const dialog = document.getElementById("dialog");
const dialog_ok = document.getElementById("dialog_ok");
const dialog_abbr = document.getElementById("dialog_abbr");
var erstaufruf = true;
button.addEventListener("click", function(event){
const val = document.querySelector("input").value;
dialog.showModal();
if(erstaufruf) {
dialog_ok.addEventListener("click", function(event) {
console.log("dialog klick OK");
absenden();
dialog.close();
});
}
if(erstaufruf) {
dialog.addEventListener("keydown", function(event) {
console.log("dialog keydown");
if(event.keyCode == 13) {
console.log("dialog keydown OK");
event.preventDefault();
absenden();
dialog.close();
}
});
}
erstaufruf = false;
console.log("Testausgabe val vor absenden(): "+val);
function absenden() {
console.log("Senden: "+val);
}
});
})();
</script>
</body>
</html>
Die Funktion absenden() ist auch innerhalb der anonymen Funktion definiert und soll eigentlich immer auf den aktuellen val zugreifen. Sie nutzt aber immer das val vom ersten absenden. Die Testausgabe direkt vor function ausgabe() liefert den korrekten, aktuellen val, nur die Funktion selbst dann nicht mehr.
Schuld ist die Schalterzeile if(erstaufruf) { Ich verstehe nur noch nicht ganz, warum sie function absenden() beeinflusst. Es wird anscheinend immer der Aufruf absenden() vom ersten Aufruf verwendet. Ist das logisch, ja? Muss ja...
Es wird wohl Zeit, Feierabend zu machen...
Schöne Grüße
Nico
Gunnars Vorschlag mit removeEventListener() war doch der richtige Ansatz. Ich habe dafür aus den anonymen Funktionen im addEventListener() benannte Funktionen gemacht und entferne nach dem Aufruf den EventListener direkt wieder. So konnte auch der dämliche Schalter wieder raus.
Danke für die Hilfe und schöne Grüße
Nico
Hallo Nico R.,
welcome in closure hell…
Für die erklärende Antwort brauche ich aber meinen PC, bin gerade am Handy. Kommt demnächst…
Rolf
Hallo Nico R.,
oha, ich hab mein Versprechen vergessen. Aber wie ich Felix unten erklärt habe, glaube ich nicht, dass es Dich wirklich interessiert.
Grundsätzlich ist es so, dass bei jedem Aufruf des Öffnen-Handlers ein neuer Ausführungskontext für diese Funktion entsteht. Die Variable val und auch die anonymen Funktionen, die Du an die Dialogbuttons bindest, sind an diesen Ausführungskontext gekoppelt.
Wenn Du die Handler für die Dialogbuttons nur beim ersten Klick auf Öffnen registrierst, greifen sie für alle Zeit auf die val-Instanz dieses ersten Öffnens zu.
Wenn man Handler nur einmal registriert, muss man aufpassen, wie und wo man seine Arbeitsdaten speichert, damit alle Handler auf den gleichen Datensatz zugreifen. Dazu schreibe ich aber erst dann einen Aufsatz, wenn Du mehr über den tatsächlichen Code erzählst, in dessen Umfeld das abläuft. Eine Online-Testinstanz deiner Seite, ohne schützenswerte persönliche Daten, würde hier helfen.
Rolf
Hallo Nico,
Aber warum registstrierst du die überhaupt im Eventhandler? Wenn du die da rausnimmst, sollte sich das Problem erledigt haben.
Mit anderen Worten: registrier sie einfach da, wo Du let erstaufruf=true stehen hast. Ohne once-Option. Das Dialog-Element bleibt auch nach dem Close vorhanden, die Eventhandler auch.
Deregistrieren gänge rein theoriesam natürlich auch, ist aber (a) viel zu umständlich und geht (b) nicht so einfach mit anonymen Funktionen. Bei der Deregistrierung muss EXAKT das gleiche Funktionsobjekt verwendet werden wie bei der registrierung, und wenn die Funktionen im click-Eventhandler definiert werden, hast Du bei jedem click ein anderes Funktionsobjekt. D.h. du müsstest die Handlerfunktionen in lokalen Variablen des click-Handlers speichern, dann bleiben sie bis zum Schließen vorhanden (-> Closure)
Rolf
Lieber Nico,
button.addEventListener("click", function(event){ //... dialog_ok.addEventListener("click", function(event) { // ... }, {once: true} ); dialog.addEventListener("keydown", function(event) { // ... }, {once: true} ); });
Du willst also, dass nur dann, wenn ein spezieller Button mittels einem click
-Event bedient wird, dass zwei andere Bedienelemente einen Eventlistener zugewiesen bekommen, egal ob sie schon einen haben oder nicht. Das bedeutet, dass mit jedem Klick auf diesen einen Button immer noch mehr an Eventlistenern hinzugefügt wird.
Oder missverstehe ich da etwas?
Liebe Grüße
Felix Riesterer
Hallo Felix,
ja, das ist leider so. Was ich aber mit removeEventListener() heilen konnte. Elegant und effezient ists nicht, aber es handelt sich bei dem Script um eine Funktion für das Redaktionssystem der Seite, die nicht oft genutzt wird. Für einen Umbau der verschiedenen Schachtelungen fehlte mir einfach die Lust. Manchmal ists gut, wenns einfach läuft, find ich 😀
Schöne Grüße
Nico
Lieber Nico,
Manchmal ists gut, wenns einfach läuft, find ich 😀
also ich finde es gut, wenn es immer „einfach läuft“. 😉
Was ich aber mit removeEventListener() heilen konnte.
Soso, „heilen konnte“? Aus meiner Sicht hattest Du da etwas gebaut, das repariert gehört, weil seine Struktur nicht wirklich sinnvoll ist. Deine „Heilung“ ist in Wirklichkeit ein sogenannter Hack, oder schöner formuliert: ein „Hotfix“.
Zum Verständnis: Du willst, dass gewisse Buttons mittels Eventlistener-Funktionen gewisse Funktionalitäten erhalten. Soweit OK. Dass Du diese aber jedes Mal auf Knopfdruck erneut zuweist, anstatt beim Laden der Seite nur einmal, sodass Du sie auch jedes Mal wieder entfernen musst, anstatt sie lassen zu können, zeigt, dass Deine Vorgehensweise verbesserbar ist. Hier ein Beispiel:
document.addEventListener('DOMContentLoaded', function () {
const
button = document.getElementById("button"),
dialog = document.getElementById("dialog"),
dialog_ok = document.getElementById("dialog_ok"),
dialog_abbr = document.getElementById("dialog_abbr");
// alle Zutaten vorhanden?
if (button && dialog && dialog_ok && dialog_abbr) {
button.addEventListener("click", e => {
// nur wenn noch nicht geöffnet
if (!dialog.open) {
dialog.showModal();
}
});
dialog_abbr.addEventListener("click", e => {
console.log("Abbrechen-Button");
dialog.close();
});
dialog_ok.addEventListener("click", e => {
console.log("OK-Button");
dialog.close();
});
document.addEventListener("keydown", e => {
if (dialog.open && "Enter" == e.key) {
console.log("Enter-Taste");
dialog.close();
}
});
}
});
Das Beispiel zeigt, wie man die Eventlistener-Funktionen nur einmal dauerhaft im Dokument registriert. Das ist verlässlicher, als das nur beim Klicken eines Buttons zu tun, denn solange das Dialogfenster nicht geöffnet ist, stört es ja nicht, wenn die Eventlistener da sind, da sie im obigen Beispiel ja prüfen, ob sie gerade überhaupt gebraucht werden.
Liebe Grüße
Felix Riesterer
Hallo Felix,
ich hatte auch schon einen längeren Beitrag angedacht, der das diskutiert, und auch schon ein Fiddle angefangen. Aber Nico hat uns nur ein Beispiel für den Problemtyp gezeigt und nicht das ganze Umfeld. Da gibt's noch einiges Gefummel an Daten, wie ich verstanden habe, und ohne das zu kennen, können wir kaum erklären, wie er das korrekt machen muss.
Das Hauptproblem ist wohl, dass er bei einmaliger Registrierung mit den Daten nicht zurecht kommt, was auf einen Besuch in closure hell hindeutet. Eine abstrakte Hilfestellung ohne Kenntnis der Seite (die Nico uns nicht geben kann/will, weil es private Interna sind und er scheinbar keine öffentlich erreichbare Testinstallation hat) war mir zu komplex.
Wenn man das Thema schon angeht, dann auch gleich so, dass man ein <form method="dialog"> in den Dialog packt. In dem Fall muss man sich nur auf das submit-Event des Forms registrieren und auf das cancel-Event des Dialogs. Das eigentliche Button-Handling entfällt dann.
Im submit-Event behandelt man ok- und cancel-Button. Ich habe nicht rausgefunden, wie man einen Button konfigurieren muss, der den Dialog automatisch abbricht. Im cancel-Event landet man nach meiner Erfahrung nur, wenn der User die Esc-Taste drückt (ggf. auch bei Klick außerhalb des Dialogs).
Ich habe aber keine Ahnung, ob Nico so tief überhaupt einsteigen will, und ins Leere predigen mag ich auch nicht. Deswegen habe ich es gelassen. Auch wenn's mich gejuckt hat, das Thema ist durchaus von allgemeinem Interesse. Ich werde wohl irgendwas was dazu ins Wiki schreiben.
Rolf