Änderungen einer Form überwachen

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:

Chrome 9

Chrome 9

Firefox 3.6

Firefox 3.6

IE 8

IE 8

Safari 5

Safari 5

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.

Weitere Posts:

Dieser Beitrag wurde unter Javascript, webdev veröffentlicht. Setze ein Lesezeichen auf den Permalink.

10 Antworten auf Änderungen einer Form überwachen

  1. Oliver sagt:

    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.

    1. Oliver sagt:

      Hm, das war nix – vielleicht kannst Du es ja richtig basteln :-)

      1. david sagt:

        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.

        1. Oliver sagt:

          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.

  2. Tom sagt:

    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

    1. Oliver sagt:

      Die Fehlermeldung wäre sicher gut. :-)

      1. Tom sagt:

        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 ;-)

        1. Oliver sagt:

          Stimmt! :-)

          $(„form“).bind(„submit“, function() {
          window.onbeforeunload = null;
          });

      2. Tom sagt:

        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 ;-)

        1. lubb sagt:

          $(‚#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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert