{"id":498,"date":"2011-02-20T13:12:29","date_gmt":"2011-02-20T12:12:29","guid":{"rendered":"https:\/\/d-mueller.de\/blog\/?p=498"},"modified":"2016-01-11T23:30:33","modified_gmt":"2016-01-11T22:30:33","slug":"aenderungen-einer-form-ueberwachen","status":"publish","type":"post","link":"https:\/\/d-mueller.de\/blog\/aenderungen-einer-form-ueberwachen\/","title":{"rendered":"\u00c4nderungen einer Form \u00fcberwachen"},"content":{"rendered":"<p><b>Aufgabenstellung:<\/b> Ich m\u00f6chte eine &lt;form&gt; mit allen ihren Elementen \u00fcberwachen, ob der Benutzer eine \u00c4nderung vorgenommen hat. Wenn dem so ist und er die Seite verl\u00e4sst \/ neu l\u00e4dt, ohne die Form abgeschickt zu haben, soll ein Hinweis erscheinen, der ihn darauf aufmerksam macht und fragt, ob er die Seite wirklich verlassen m\u00f6chte.<\/p>\n<h2>Das HTML-Ger\u00fcst<\/h2>\n<p>Mal alles hingezimmert, was wir so an Formularelementen kennen:<\/p>\n<pre data-enlighter-language=\"html\" class=\"EnlighterJSRAW\">\r\n&lt;!DOCTYPE html&gt;\r\n&lt;html&gt;\r\n&lt;head&gt;\r\n\t&lt;script src=&quot;http:\/\/code.jquery.com\/jquery-1.5.js&quot;&gt;&lt;\/script&gt;\r\n\t&lt;script&gt;\r\n\t\/\/Code folgt dann hier\r\n\t&lt;\/script&gt;\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n\t&lt;form&gt;\r\n\t\t&lt;select name=&quot;select-single&quot; id=&quot;select-single&quot;&gt;\r\n\t\t\t&lt;option&gt;Single1&lt;\/option&gt;\r\n\t\t\t&lt;option&gt;Single2&lt;\/option&gt;\r\n\t\t&lt;\/select&gt;\r\n\t\t&lt;select name=&quot;select-multiple&quot; id=&quot;select-multiple&quot; multiple=&quot;multiple&quot;&gt;\r\n\t\t\t&lt;option selected=&quot;selected&quot;&gt;Multiple1&lt;\/option&gt;\r\n\t\t\t&lt;option&gt;Multiple2&lt;\/option&gt;\r\n\t\t\t&lt;option selected=&quot;selected&quot;&gt;Multiple3&lt;\/option&gt;\r\n\t\t&lt;\/select&gt;\r\n\t\t&lt;input type=&quot;checkbox&quot; name=&quot;check&quot; value=&quot;check1&quot; id=&quot;check1&quot; \/&gt;\r\n\t\t&lt;input type=&quot;checkbox&quot; name=&quot;check&quot; value=&quot;check2&quot; checked=&quot;checked&quot; id=&quot;check2&quot; \/&gt;\r\n\t\t&lt;input type=&quot;radio&quot; name=&quot;radio&quot; value=&quot;radio1&quot; checked=&quot;checked&quot; id=&quot;radio1&quot; \/&gt;\r\n\t\t&lt;input type=&quot;radio&quot; name=&quot;radio&quot; value=&quot;radio2&quot; id=&quot;radio2&quot; \/&gt;\r\n\t\t&lt;input type=&quot;text&quot; name=&quot;text&quot; value=&quot;default text&quot; id=&quot;text&quot; \/&gt;\r\n\t\t&lt;textarea name=&quot;textarea&quot; id=&quot;textarea&quot;&gt;foobar&lt;\/textarea&gt;\r\n\t&lt;\/form&gt;\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;\r\n<\/pre>\n<p>Da w\u00e4ren 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\u00fcgung stellt.<\/p>\n<h2>Naiver erster Versuch: onchange observieren<\/h2>\n<p>Wir packen in den head:<\/p>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\r\nvar formHasChanged = false;\r\n\r\n$(document).ready(function()\r\n{\r\n\t$(&quot;select, input, textarea&quot;).change(function()\r\n\t{\r\n\t\tformHasChanged = true;\r\n\t});\r\n});\r\n<\/pre>\n<p>Ver\u00e4ndert der Benutzer also irgendwas, wird die globale Variable <i>formHasChanged<\/i> &#8222;getrue&#8217;t&#8220;. <b>Problem dabei:<\/b> Wenn der Benutzer jetzt bspw. etwas in die Textarea tippt, dann aber doch wieder rausl\u00f6scht hat er effektiv nichts ge\u00e4ndert. Unsere naive L\u00f6sung 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\u00f6chte (<a href=\"https:\/\/developer.mozilla.org\/en\/DOM\/window.onbeforeunload\">geklaut und leicht abgewandelt von hier<\/a>). Brauchen wir auch in allen anderen, noch folgenden Ans\u00e4tzen:<\/p>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\r\nwindow.onbeforeunload = function (e) \r\n{\r\n\tif (!formHasChanged)\r\n\t\treturn;\r\n\t\t\r\n\tvar e = e || window.event,\r\n\t\tstr = &#039;Wollen Sie die Seite wirklich verlassen, ohne die \u00c4nderungen zu speichern?&#039;;\r\n\r\n\t\/\/ For IE and Firefox prior to version 4\r\n\tif (e) \r\n\t\te.returnValue = str;\r\n  \r\n\t\/\/ For Safari\r\n\treturn str;\r\n}\r\n<\/pre>\n<p>Schema dabei: Wenn sich die Form nicht ver\u00e4ndert hat, returnen wir gleich wieder und lassen den Benutzer die Seite verlassen, ohne das er etwas best\u00e4tigen muss. Sollte er etwas ver\u00e4ndert haben, greift die Funktion mit ihren Mechanismen f\u00fcr die verschiedenen Browser. Sieht \u00fcbrigens so aus:<\/p>\n<div id=\"attachment_499\" style=\"width: 380px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/chrome.png\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-499\" src=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/chrome.png\" alt=\"Chrome 9\" title=\"Chrome 9\" width=\"370\" height=\"151\" class=\"size-full wp-image-499\" srcset=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/chrome.png 370w, https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/chrome-300x122.png 300w\" sizes=\"auto, (max-width: 370px) 100vw, 370px\" \/><\/a><p id=\"caption-attachment-499\" class=\"wp-caption-text\">Chrome 9<\/p><\/div>\n<div id=\"attachment_500\" style=\"width: 563px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ff36.png\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-500\" src=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ff36.png\" alt=\"Firefox 3.6\" title=\"Firefox 3.6\" width=\"553\" height=\"180\" class=\"size-full wp-image-500\" srcset=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ff36.png 553w, https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ff36-300x97.png 300w\" sizes=\"auto, (max-width: 553px) 100vw, 553px\" \/><\/a><p id=\"caption-attachment-500\" class=\"wp-caption-text\">Firefox 3.6<\/p><\/div>\n<div id=\"attachment_501\" style=\"width: 391px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ie8.png\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-501\" src=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ie8.png\" alt=\"IE 8\" title=\"IE 8\" width=\"381\" height=\"261\" class=\"size-full wp-image-501\" srcset=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ie8.png 381w, https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/ie8-300x205.png 300w\" sizes=\"auto, (max-width: 381px) 100vw, 381px\" \/><\/a><p id=\"caption-attachment-501\" class=\"wp-caption-text\">IE 8<\/p><\/div>\n<div id=\"attachment_502\" style=\"width: 511px\" class=\"wp-caption alignnone\"><a href=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/safari.png\"><img loading=\"lazy\" decoding=\"async\" aria-describedby=\"caption-attachment-502\" src=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/safari.png\" alt=\"Safari 5\" title=\"Safari 5\" width=\"501\" height=\"239\" class=\"size-full wp-image-502\" srcset=\"https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/safari.png 501w, https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/safari-300x143.png 300w\" sizes=\"auto, (max-width: 501px) 100vw, 501px\" \/><\/a><p id=\"caption-attachment-502\" class=\"wp-caption-text\">Safari 5<\/p><\/div>\n<p>In Opera hauts aktuell noch nicht hin. Erw\u00e4hnt sei auch, dass ein Schlie\u00dfen des Browsers den Mechanismus nicht triggert.<\/p>\n<h2>Zweite L\u00f6sung: Form serialisieren<\/h2>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\r\nvar initial = &quot;&quot;;\r\n\r\n$(document).ready(function()\r\n{\r\n\tinitial = $(&quot;form&quot;).serialize();\r\n});\r\n\r\nfunction hasChanged() \r\n{\r\n\treturn !(initial === $(&quot;form&quot;).serialize());\r\n}\r\n\r\nwindow.onbeforeunload = function (e) \r\n{\r\n\tif (!hasChanged())\r\n\t\treturn;\r\n\t\t\r\n\tvar e = e || window.event,\r\n\t\tstr = &#039;Wollen Sie die Seite wirklich verlassen, ohne die \u00c4nderungen zu speichern?&#039;;\r\n\r\n\t\/\/ For IE and Firefox prior to version 4\r\n\tif (e) \r\n\t\te.returnValue = str;\r\n  \r\n\t\/\/ For Safari\r\n\treturn str;\r\n}\r\n<\/pre>\n<p>Simples Prinzip: Wir serialisieren die Form, wenn der Benutzer die Seite betritt. Sieht im Beispiel oben so aus:<\/p>\n<pre data-enlighter-language=\"enlighter\" class=\"EnlighterJSRAW\">\r\nselect-single=Single1&amp;select-multiple=Multiple1&amp;select-multiple=Multiple3&amp;check=check2&amp;radio=radio1&amp;text=default+text&amp;textarea=foobar\r\n<\/pre>\n<p>Verl\u00e4sst der Benutzer nun die Seite, wird erneut serialisiert und die beiden Strings verglichen. Klappt gut.<\/p>\n<h2>L\u00f6sung 3: Default-Wert pr\u00fcfen<\/h2>\n<p>Konzept: Bevor der Benutzer die Seite verlassen will, \u00fcberpr\u00fcfen 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\u00dft die M\u00f6glichkeit dazu bei allen Elementen etwas anders: <\/p>\n<h3>defaultValue bei textarea \/ input type=text<\/h3>\n<pre data-enlighter-language=\"html\" class=\"EnlighterJSRAW\">\r\n&lt;input type=&quot;text&quot; id=&quot;textfield&quot; value=&quot;Wert 1&quot; \/&gt;\r\n&lt;textarea id=&quot;textarea&quot;&gt;Wert 2&lt;\/textarea&gt;\r\n&lt;script&gt;\r\nalert(document.getElementById(&quot;textfield&quot;).defaultValue); \/\/Wert 1\r\nalert(document.getElementById(&quot;textarea&quot;).defaultValue); \/\/Wert 2\r\n&lt;\/script&gt;\r\n<\/pre>\n<p>Damit l\u00e4sst sich doch Arbeiten. <\/p>\n<h3>defaultChecked bei Radiobutton \/ Checkbox<\/h3>\n<pre data-enlighter-language=\"html\" class=\"EnlighterJSRAW\">\r\n&lt;input type=&quot;checkbox&quot; checked=&quot;checked&quot; id=&quot;check&quot; \/&gt;\r\n&lt;input type=&quot;radio&quot; id=&quot;radio&quot; \/&gt;\r\n&lt;script&gt;\r\nalert(document.getElementById(&quot;check&quot;).defaultChecked); \/\/true\r\nalert(document.getElementById(&quot;radio&quot;).defaultChecked); \/\/false\r\n&lt;\/script&gt;\r\n<\/pre>\n<h3>Etwas komplizierter bei Selects<\/h3>\n<p>Selects gibts ja grunds\u00e4tzlich mit oder ohne Mehrfachauswahl. Deswegen m\u00fcssen wir hier schonmal aufpassen. Weiterhin hei\u00dft unser gesuchtes Attribut <i>defaultSelected<\/i> und ist folgenderma\u00dfen abzugreifen:<\/p>\n<pre data-enlighter-language=\"html\" class=\"EnlighterJSRAW\">\r\n&lt;select id=&quot;sel&quot; multiple=&quot;multiple&quot;&gt;\r\n\t&lt;option selected=&quot;selected&quot;&gt;Multiple1&lt;\/option&gt;\r\n\t&lt;option&gt;Multiple2&lt;\/option&gt;\r\n\t&lt;option selected=&quot;selected&quot;&gt;Multiple3&lt;\/option&gt;\r\n&lt;\/select&gt;\r\n\r\n&lt;script&gt;\r\nvar sel = document.getElementById(&quot;sel&quot;);\r\nfor (var i = 0; i &lt; sel.options.length; i++) \r\n{\r\n\talert(sel.options[i].defaultSelected); \/\/true, false, true\r\n}\r\n&lt;\/script&gt;\r\n<\/pre>\n<p>Analog ist es beim Select ohne Mehrfachauswahl.<\/p>\n<h3>Jetzt kombinieren wir das alles zusammen<\/h3>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\r\nvar FormChangeChecker = \r\n{\r\n\tselectHasChanged: function (domelem)\r\n\t{\r\n\t\tvar selectchanged = false, \r\n\t\t\tdef = 0;\r\n\t\t\r\n\t\tfor (var i = 0; i &lt; domelem.options.length; i++) \r\n\t\t{\r\n\t\t\tif (domelem.options[i].selected != domelem.options[i].defaultSelected)\r\n\t\t\t\tselectchanged = true;\r\n\t\t\t\t\r\n\t\t\tif (domelem.options[i].defaultSelected) \r\n\t\t\t\tdef = i;\r\n\t\t}\r\n\t\t\r\n\t\tif (selectchanged &amp;&amp; !domelem.multiple) \r\n\t\t\tselectchanged = (def != domelem.selectedIndex);\r\n\t\t\t\r\n\t\treturn selectchanged;\r\n\t},\r\n\t\r\n\ttextinputOrTextareaHasChanged: function (domelem)\r\n\t{\r\n\t\treturn domelem.value != domelem.defaultValue;\r\n\t},\r\n\t\r\n\tradioOrCheckboxHasChanged: function (domelem)\r\n\t{\r\n\t\treturn domelem.checked != domelem.defaultChecked;\r\n\t},\r\n\t\r\n\tformHasChanged: function ()\r\n\t{\r\n\t\tvar formWasChanged = false;\r\n\t\t\r\n\t\t$(&quot;select&quot;).each(function()\r\n\t\t{\r\n\t\t\tif (FormChangeChecker.selectHasChanged(this))\r\n\t\t\t\tformWasChanged = true;\r\n\t\t});\r\n\t\t\r\n\t\t$(&quot;input[type=text], textarea&quot;).each(function()\r\n\t\t{\r\n\t\t\tif (FormChangeChecker.textinputOrTextareaHasChanged(this))\r\n\t\t\t\tformWasChanged = true;\r\n\t\t});\r\n\t\t\r\n\t\t$(&quot;input[type=radio], input[type=checkbox]&quot;).each(function()\r\n\t\t{\r\n\t\t\tif (FormChangeChecker.radioOrCheckboxHasChanged(this))\r\n\t\t\t\tformWasChanged = true;\r\n\t\t});\r\n\t\t\r\n\t\treturn formWasChanged;\r\n\t}\r\n};\r\n<\/pre>\n<p>Ist jetzt eigentlich trivial. Es gibt eine Funktion zum \u00fcberpr\u00fcfen, ob sich ein Select ge\u00e4ndert hat (dabei erschl\u00e4gt die Funktion sowohl Selects mit als auch ohne Mehrfachauswahl). Dann haben wir eine Funktion zum \u00dcberpr\u00fcfen, ob sich eine Checkbox \/ ein Radiobutton ge\u00e4ndert hat. Schlussendlich noch dasselbe f\u00fcr Textfelder und Textareas. Das sind also nur die bereits durchgekauten Codebeispiele von weiter oben. Jetzt flie\u00dft das alles zusammen in der Funktion <b>formHasChanged()<\/b>. Wir gehen alle Arten von Elementen durch und stecken sie in die jeweilige Funktion. Damit brauchen wir nur noch unser bereits bekanntes Snippet <b>window.onbeforeunload<\/b>:<\/p>\n<pre data-enlighter-language=\"js\" class=\"EnlighterJSRAW\">\r\nwindow.onbeforeunload = function (e) \r\n{\r\n\tif (!FormChangeChecker.formHasChanged())\r\n\t\treturn;\r\n\t\t\r\n\tvar e = e || window.event,\r\n\t\tstr = &#039;Wollen Sie die Seite wirklich verlassen, ohne die \u00c4nderungen zu speichern?&#039;;\r\n\r\n\t\/\/ For IE and Firefox prior to version 4\r\n\tif (e) \r\n\t\te.returnValue = str;\r\n  \r\n\t\/\/ For Safari\r\n\treturn str;\r\n}\r\n<\/pre>\n<h2>Okay, und was soll soll man jetzt verwenden?<\/h2>\n<p>Die zweite Variante mit dem simplen serialize hat das Problem, dass man nicht einfach feststellen kann, <b>welches<\/b> Element sich genau ver\u00e4ndert hat, sondern nur das sich irgendwas ver\u00e4ndert hat. Wenn wir das noch feingranular gestalten m\u00fcssen (etwa das Element highlighten, welches ver\u00e4ndert wurde), kann also die default-value Variante die bessere sein. Grenzen haben die L\u00f6sungen zudem, wenn Elemente w\u00e4hrend der Laufzeit neu hinzugef\u00fcgt werden. Aber auch das sollte mit etwas Anpassung zu bew\u00e4ltigen sein. Wer jetzt ohne fieses zusammenkopieren alle 3 Varianten zum herumspielen haben m\u00f6chte, der <a href='https:\/\/d-mueller.de\/blog\/wp-content\/uploads\/2011\/02\/formChangeObservation.zip'>findet hier eine zip<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Aufgabenstellung: Ich m\u00f6chte eine &lt;form&gt; mit allen ihren Elementen \u00fcberwachen, ob der Benutzer eine \u00c4nderung vorgenommen hat. Wenn dem so ist und er die Seite verl\u00e4sst \/ neu l\u00e4dt, ohne die Form abgeschickt zu haben, soll ein Hinweis erscheinen, der &hellip; <a href=\"https:\/\/d-mueller.de\/blog\/aenderungen-einer-form-ueberwachen\/\">Weiterlesen <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12,3],"tags":[],"class_list":["post-498","post","type-post","status-publish","format-standard","hentry","category-javascript","category-webdev"],"_links":{"self":[{"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/posts\/498","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/comments?post=498"}],"version-history":[{"count":0,"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/posts\/498\/revisions"}],"wp:attachment":[{"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/media?parent=498"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/categories?post=498"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/d-mueller.de\/blog\/wp-json\/wp\/v2\/tags?post=498"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}