Aufgabenstellung: Ich möchte eine <form> mit allen ihren Elementen überwachen, ob der Benutzer eine Änderung vorgenommen hat. Wenn dem so ist und er die Seite verlässt / neu lädt, ohne die Form abgeschickt zu haben, soll ein Hinweis erscheinen, der ihn darauf aufmerksam macht und fragt, ob er die Seite wirklich verlassen möchte.
Das HTML-Gerüst
Mal alles hingezimmert, was wir so an Formularelementen kennen:
<!DOCTYPE html> <html> <head> <script src="http://code.jquery.com/jquery-1.5.js"></script> <script> //Code folgt dann hier </script> </head> <body> <form> <select name="select-single" id="select-single"> <option>Single1</option> <option>Single2</option> </select> <select name="select-multiple" id="select-multiple" multiple="multiple"> <option selected="selected">Multiple1</option> <option>Multiple2</option> <option selected="selected">Multiple3</option> </select> <input type="checkbox" name="check" value="check1" id="check1" /> <input type="checkbox" name="check" value="check2" checked="checked" id="check2" /> <input type="radio" name="radio" value="radio1" checked="checked" id="radio1" /> <input type="radio" name="radio" value="radio2" id="radio2" /> <input type="text" name="text" value="default text" id="text" /> <textarea name="textarea" id="textarea">foobar</textarea> </form> </body> </html>
Da wären also ein Select, ein Select mit Mehrfachauswahl, ein Textfeld, eine Textarea, zwei Radiobuttons und zwei Checkboxen. Das sollte alles gewesen sein, was uns HTML 4 so zur Verfügung stellt.
Naiver erster Versuch: onchange observieren
Wir packen in den head:
var formHasChanged = false; $(document).ready(function() { $("select, input, textarea").change(function() { formHasChanged = true; }); });
Verändert der Benutzer also irgendwas, wird die globale Variable formHasChanged „getrue’t“. Problem dabei: Wenn der Benutzer jetzt bspw. etwas in die Textarea tippt, dann aber doch wieder rauslöscht hat er effektiv nichts geändert. Unsere naive Lösung rafft das aber nicht. Also nicht toll. Jetzt fehlt aber noch die Magie dazu, die den Benutzer beim Verlassen der Seite fragt, ob er nicht doch bleiben möchte (geklaut und leicht abgewandelt von hier). Brauchen wir auch in allen anderen, noch folgenden Ansätzen:
window.onbeforeunload = function (e) { if (!formHasChanged) return; var e = e || window.event, str = 'Wollen Sie die Seite wirklich verlassen, ohne die Änderungen zu speichern?'; // For IE and Firefox prior to version 4 if (e) e.returnValue = str; // For Safari return str; }
Schema dabei: Wenn sich die Form nicht verändert hat, returnen wir gleich wieder und lassen den Benutzer die Seite verlassen, ohne das er etwas bestätigen muss. Sollte er etwas verändert haben, greift die Funktion mit ihren Mechanismen für die verschiedenen Browser. Sieht übrigens so aus:
In Opera hauts aktuell noch nicht hin. Erwähnt sei auch, dass ein Schließen des Browsers den Mechanismus nicht triggert.
Zweite Lösung: Form serialisieren
var initial = ""; $(document).ready(function() { initial = $("form").serialize(); }); function hasChanged() { return !(initial === $("form").serialize()); } window.onbeforeunload = function (e) { if (!hasChanged()) return; var e = e || window.event, str = 'Wollen Sie die Seite wirklich verlassen, ohne die Änderungen zu speichern?'; // For IE and Firefox prior to version 4 if (e) e.returnValue = str; // For Safari return str; }
Simples Prinzip: Wir serialisieren die Form, wenn der Benutzer die Seite betritt. Sieht im Beispiel oben so aus:
select-single=Single1&select-multiple=Multiple1&select-multiple=Multiple3&check=check2&radio=radio1&text=default+text&textarea=foobar
Verlässt der Benutzer nun die Seite, wird erneut serialisiert und die beiden Strings verglichen. Klappt gut.
Lösung 3: Default-Wert prüfen
Konzept: Bevor der Benutzer die Seite verlassen will, überprüfen wir alle Elemente, ob ihr aktueller Wert von ihrem Default-Wert abweicht. War mir vor kurzem noch nicht bekannt, dass das mit Bordmitteln geht! Leider heißt die Möglichkeit dazu bei allen Elementen etwas anders:
defaultValue bei textarea / input type=text
<input type="text" id="textfield" value="Wert 1" /> <textarea id="textarea">Wert 2</textarea> <script> alert(document.getElementById("textfield").defaultValue); //Wert 1 alert(document.getElementById("textarea").defaultValue); //Wert 2 </script>
Damit lässt sich doch Arbeiten.
defaultChecked bei Radiobutton / Checkbox
<input type="checkbox" checked="checked" id="check" /> <input type="radio" id="radio" /> <script> alert(document.getElementById("check").defaultChecked); //true alert(document.getElementById("radio").defaultChecked); //false </script>
Etwas komplizierter bei Selects
Selects gibts ja grundsätzlich mit oder ohne Mehrfachauswahl. Deswegen müssen wir hier schonmal aufpassen. Weiterhin heißt unser gesuchtes Attribut defaultSelected und ist folgendermaßen abzugreifen:
<select id="sel" multiple="multiple"> <option selected="selected">Multiple1</option> <option>Multiple2</option> <option selected="selected">Multiple3</option> </select> <script> var sel = document.getElementById("sel"); for (var i = 0; i < sel.options.length; i++) { alert(sel.options[i].defaultSelected); //true, false, true } </script>
Analog ist es beim Select ohne Mehrfachauswahl.
Jetzt kombinieren wir das alles zusammen
var FormChangeChecker = { selectHasChanged: function (domelem) { var selectchanged = false, def = 0; for (var i = 0; i < domelem.options.length; i++) { if (domelem.options[i].selected != domelem.options[i].defaultSelected) selectchanged = true; if (domelem.options[i].defaultSelected) def = i; } if (selectchanged && !domelem.multiple) selectchanged = (def != domelem.selectedIndex); return selectchanged; }, textinputOrTextareaHasChanged: function (domelem) { return domelem.value != domelem.defaultValue; }, radioOrCheckboxHasChanged: function (domelem) { return domelem.checked != domelem.defaultChecked; }, formHasChanged: function () { var formWasChanged = false; $("select").each(function() { if (FormChangeChecker.selectHasChanged(this)) formWasChanged = true; }); $("input[type=text], textarea").each(function() { if (FormChangeChecker.textinputOrTextareaHasChanged(this)) formWasChanged = true; }); $("input[type=radio], input[type=checkbox]").each(function() { if (FormChangeChecker.radioOrCheckboxHasChanged(this)) formWasChanged = true; }); return formWasChanged; } };
Ist jetzt eigentlich trivial. Es gibt eine Funktion zum überprüfen, ob sich ein Select geändert hat (dabei erschlägt die Funktion sowohl Selects mit als auch ohne Mehrfachauswahl). Dann haben wir eine Funktion zum Überprüfen, ob sich eine Checkbox / ein Radiobutton geändert hat. Schlussendlich noch dasselbe für Textfelder und Textareas. Das sind also nur die bereits durchgekauten Codebeispiele von weiter oben. Jetzt fließt das alles zusammen in der Funktion formHasChanged(). Wir gehen alle Arten von Elementen durch und stecken sie in die jeweilige Funktion. Damit brauchen wir nur noch unser bereits bekanntes Snippet window.onbeforeunload:
window.onbeforeunload = function (e) { if (!FormChangeChecker.formHasChanged()) return; var e = e || window.event, str = 'Wollen Sie die Seite wirklich verlassen, ohne die Änderungen zu speichern?'; // For IE and Firefox prior to version 4 if (e) e.returnValue = str; // For Safari return str; }
Okay, und was soll soll man jetzt verwenden?
Die zweite Variante mit dem simplen serialize hat das Problem, dass man nicht einfach feststellen kann, welches Element sich genau verändert hat, sondern nur das sich irgendwas verändert hat. Wenn wir das noch feingranular gestalten müssen (etwa das Element highlighten, welches verändert wurde), kann also die default-value Variante die bessere sein. Grenzen haben die Lösungen zudem, wenn Elemente während der Laufzeit neu hinzugefügt werden. Aber auch das sollte mit etwas Anpassung zu bewältigen sein. Wer jetzt ohne fieses zusammenkopieren alle 3 Varianten zum herumspielen haben möchte, der findet hier eine zip.
Du könntest auch data() nutzen. Schnell, dreckig und (wahrscheinlich) funktionsuntüchtig:
$("input,select,textarea").each(function(){
$(this).data('initial', $(this).value()+$(this).checked()+$(this).selected());
});
// und später
$("input,select,textarea").each(function(){
return $(this).data('initial') == $(this).value()+$(this).checked()+$(this).selected();
});
Das hätte den Vorteil, dass Du gezielt auf das Feld springen kannst, das geändert wurde bzw. Du könntest anzeigen, welche oder wie viele Felder geändert wurden.
Hm, das war nix – vielleicht kannst Du es ja richtig basteln :-)
Hi Oliver! Mein Syntax-Highlighter ist seit dem WordPress-Update auf 3.05 einem Bug erlegen. Dein Ansatz könnte auch gut funktionieren, definitiv. Ich würd mich damit bloß in der Form noch nicht sonderlich wohl damit fühlen, weil du checked() und selected() bspw. auch auf Textareas aufrufst. Außerdem denke ich nicht, dass es bei multiple selects klappen würde. Aber das liese sich schon noch reinfuddeln.
Geht wieder :-)
Was ich sagen wollte … checked() und selected() auf Input/Textareas geben ja nur false zurück. Wenn die vorher false zurück gegeben haben, geben die nach ner Änderung immer noch false zurück.
Das ist nur eine Vereinfachung zu einer Schleife, wo jedes einzelne Element gemäß seiner Eigenschaften ausgelesen würde, was eigentlich für den Zweck überdimensioniert ist.
Hey, erstmal danke für das Script. Leider habe ich mit der zweiten Methode so meine Probleme. Ich bekomme auch eine Fehlermeldung wenn ich das Formular per Submit abschicke. Hier im Script gibt es ja gar kein Submit.
Gibt es dazu einen Lösungsvorschlag? Wäre echt super :)
Gruß Tom
Die Fehlermeldung wäre sicher gut. :-)
Es ist ja keine extra Fehlermeldung. Ich bekomme die selbe Meldung wenn ich Formulardaten ändere und das Formular über einen Link verlasse, also wie gewollt, aber auch, und das stört mich, wenn ich das Formular mit dem Submit-Button abschicke.
Ich möchte nur die Meldung erhalten wenn ich das Formular über einen Link verlasse jedoch nicht wenn ich es per Submit (oder Enter) abschicke.
Hoffe das war verständlicher ;-)
Stimmt! :-)
$(„form“).bind(„submit“, function() {
window.onbeforeunload = null;
});
Folgendes scheint (zumindest im FF und IE) das Submit-Problem zu lösen:
$('#formularID').submit(function() {
window.onbeforeunload = function(e) {}
});
Leider funktioniert es noch nicht in Opera, wäre auf jeden Fall noch das I-Tüpfelchen ;-)
$(‚#formularID‘).submit(function() {
window.onbeforeunload = function(e) {}
});
geht leider IE nicht, hat ihr das getestet
gibts es eine Alternativ-Lösung
Beste Grüss
Lubb