borisbaer: JavaScript greift auf bereits entfernte DOM-Elemente zu?

Beitrag lesen

problematische Seite

Hallo Rolf,

Hattest Du den changePage-Aufruf, den Du derzeit im Timer hast, vorher in dem .then-Aufruf des fetch, wo Du das DOM veränderst?

        .then( ( html ) => { 
             mapsWrapper.innerHTML = html;
             changePage( true );
        })

sollte eigentlich funktionieren.

danke, das funktioniert in der Tat! Ich hatte vergessen, dass ich den Funktionsaufruf in die geschweifte Klammer aufnehmen muss. Stattdessen hatte ich ihn zwischen .then und .catch. 😯

Und ja, changePage(true), nicht changePage(reset=true). Du tust so, als wäre das ein Funktionsparameter. Aber changePage nimmt gar keinen Parameter entgegen, statt dessen definiert change-page.js eine globale Variable reset und die setzt Du während der Parameterübergabe flugs.

Ändere change-page.js so, dass let reset; wegfällt und schreibe dort

const changePage = (reset) => {
   ...
}

Dann klappt das auch mit der Übergabe eines reset-Parameters.

Habe ich gemacht, danke für den Hinweis! Da müsste ich mich auch tatsächlich mal genauer mit beschäftigen, wie das mit den Funktionsparametern in JS funktioniert.

Deine click-Handler Steuerung ist gruselig, du fummelst da instinktiv mit Closures herum und weißt vermutlich nicht einmal, was das ist.

Ertappt! Aber auch das werde ich mir bei Gelegenheit zu Gemüte führen.

Wenn Du die maps Subpage erstmalig aufrufst, wird changePage ausgeführt. Darin setzt Du eine Variable page, mit einer Liste der Karten, die gezeigt werden. Und dann erzeugst Du eine newPage-Funktion - lokal zu changePage - und registrierst anonyme Funktionen - lokal zu changePage - als Eventhandler.

Bis hierhin habe ich es verstanden …

Damit beginnt das Drama, denn die newPage-Funktion, die so entstanden ist, enthält einen Verweis auf den Variablenkontext, in dem sie definiert wurde. Das sind die lokalen Variablen von dem changePage-Aufruf, der diese newPage Funktion erzeugt hat. newPage schließt diese Variablen mit ein - das ist die Closure.

Also handelt es sich bei diesen lokalen Variablen um eine ganz spezifische Zuordnung zu ganz bestimmten Elementen im DOM? Habe ich das richtig verstanden?

changePage endet, aber die click- und keydown-Handler bleiben. Und damit auch die lokalen Variablen von changePage. Deswegen und NUR deswegen funktioniert das Blättern, die Closure mit den Variablen von changePage liefert alles nötige.

Soweit, so gut, aber wenn Du nun einen neuen Kartensatz lädst, rufst Du changePage(true) auf - für den Reset. Den führst Du aber nicht vollständig aus. Die alten click-Handler auf den Pfeilen und auch der keydown-Handler auf dem Body bleiben erhalten, und damit auch die Closure mit den page-Verweisen auf die vorige Karte. Diese Eventhandler laufen dann auf der neuen Karte munter mit, und crashen natürlich, weil die Kartenseiten aus dem DOM gelöscht wurden und keinen parentNode mehr haben.

Der neue changePage Aufruf definiert wiederum eine newPage Funktion. Beachte: Dies ist eine andere Funktion wie die aus dem ersten changePage Aufruf. Sie ist lokal zu changePage, sie existiert nur in diesem einen Aufrufkontext. Die lokale Variable, in der Du sie speicherst, heißt zwar gleich, aber trotzdem ist es eine andere Funktion. Der Hauptunterschied ist: Die Closure über die lokalen changePage-Variablen ist eine andere.

Heißt das, dass bei jedem Aufruf der Mutterfunktion (changePage) eine neue Kindfunktion(newPage) erstellt wird? newPage bekommt also bei jedem Wechsel des Kartensatzes eine neue Instanz, nicht? Und nur die jeweils aktuelle Instanz funktioniert, die alten Instanzen laufen gewissermaßen ins Leere.

Wie kommt man da jetzt heraus?

In dem Moment, wo Du die Maps Seite verlässt, werden die Pfeile aus dem DOM gelöscht und damit auch alle Eventhandler, die daran hängen. Der keydown-Handler auf dem Body bleibt aber erhalten - der kriegt Dich auch dann noch am Allerwertesten, wenn Du die Subpage wechselst, z.B. nach Mods und wieder zurück. Nur - das merkst Du nicht. Du hast die komplette Subpage weggeschmissen, inclusive des draggable Wrappers, und dieser Zombie-Verein klammert sich jetzt verzweifelt über eine Closure am keydown-Handler fest und blättert fleißig im Nichts vor sich hin.

Also: erstmal dringend den keydown-Handler vom Body auf den draggable Wrapper verlegen, damit der einen Subpage-Wechsel nicht überlebt.

Kapiert. Jetzt habe ich nur das Problem, dass eigentlich beim Umblättern mit den Pfeil-Tasten der Fokus auf den jeweiligen Pfeil-Button gelegt werden soll. Dann muss ich aber jedes Mal den draggable Wrapper neu fokussieren, um weiter mit den Pfeiltasten navigieren zu können.
Ich muss mir mal eine Lösung dafür überlegen. Ist aber jetzt erst mal nebensächlich.

Und nun wird's schwieriger: Das Entfernen der alten Eventhandler. Das ist de facto so schwierig, dass ich für den Wechsel der abgefragten Karte generell davon abraten würde. Versuche statt dessen, die Eventhandler für die Links- und Rechtsklicks sowie den keydown-Handler so zu bauen, dass sie keine Kontextinformationen brauchen und alles, was sie brauchen, aus dem aktuellen DOM beziehen.

Leider habe ich keine Ahnung, was genau du damit meinst bzw. wie ich das umsetzen sollte. 🙁 Sie dürfen keine Kontextinformationen brauchen … hmm.

Wenn das erreicht ist, kannst Du auf Closure-Daten aus dem changePage Aufruf verzichten und brauchst die Eventhandler nicht neu zu registrieren. Das wäre mein Vorgehen. Trivial ist es nicht, aber ich denke, ziemlich robust. Die Alternative wäre ein Datenspeicher mit den erforderlichen Steuerinformationen für die Eventhandler, den Du beim Wechsel des Kartensets aktualisieren müsstest. Oder ein globaler Speicher für die alten Eventhandler-Funktionen, womit du diese deregistrieren könntest. Alles Brrr. Hantieren auf dem aktuellen DOM ist der einfachste Weg.

Klingt für mich auch zu kompliziert für das, was erreicht werden soll.

Du kannst auch DOM Objekten eigene Eigenschaften hinzufügen.

const wrap = document.querySelector(".maps .wrapper");
wrap.boris.daten = {
   foo: 1,
   bar: 2
};

Und schon hat das Wrapper div eine Eigenschaft boris, die außer Dir keiner kennt und wo Du Dinge speichern kannst. Z.B. wieviele Kartenteile Du hast und welches Teil aktiv ist. Aber gerade das brauchst Du ja eigentlich nicht. Du kannst mit querySelectorAll die Anzahl der Kartenteile finden, und die gerade angezeigte Karte kannst Du mit dem Attribut aria-selected=true markieren und wiederfinden. Du solltest auf ein Anhängsel dieser Art nach aller Möglichkeit verzichten.

Sehr interessant, vielleicht nicht für das aktuelle Vorhaben so relevant, aber damit werde ich definitiv mal herumexperimentieren. Danke für den Hinweis.

Gruß
Boris