45

I have an HTML form with over 20 fields. I also have a couple of links on the page which will lead the user away from the form... potentially without having saved any changes.

I want to warn (JS confirm) the user onClick of these links if any of the form fields have changed, but I don't want to create a huge switch statement that I then need to maintain as I add new fields to the form. I know how to create a long list of 'if' statements in Javascript, naming each of the fields and checking each value, but I don't want to do that if I can get away with it.

What's the easiest way to check if the user has changed at least one of the field values?

Marcus
  • 5,772
  • 8
  • 35
  • 60

8 Answers8

72

Approach

  1. serialize the form (and all its values) before showing it (jQuery way, Prototype way)
  2. serialize it again in a "onbeforeunload" event handler

If the two don't match, then they must've changed the form, so return a string (eg "You have unsaved data") from your onbeforeunload handler.

This method allows the form fields to evolve while the "confirm if changed" logic remains the same.

Example (mixed javascript and jquery)

var form_clean;

// serialize clean form
$(function() { 
    form_clean = $("form").serialize();  
});

// compare clean and dirty form before leaving
window.onbeforeunload = function (e) {
    var form_dirty = $("form").serialize();
    if(form_clean != form_dirty) {
        return 'There is unsaved form data.';
    }
};
Tapper
  • 1,393
  • 17
  • 28
Crescent Fresh
  • 115,249
  • 25
  • 154
  • 140
  • Ooo, that is a really sweet idea. Nice. – Brian MacKay Apr 12 '12 at 16:10
  • 13
    In 2014, is this still best practice? – Kevin Meredith Jan 04 '14 at 15:17
  • 2
    Is there a reason that the 1st line isn't just `var form_clean = $("form").serialize();`? – Edd Jan 13 '15 at 11:30
  • How would this work with dynamically added html elements? Seems like you would have to be aware anytime an action changes the DOM and re-call the serialize method. Also how could this work with libraries like knockout and angular where you don't even have to have form values inside a form? – SventoryMang Jan 04 '17 at 23:40
13

I'm pretty sure this is a bad idea, but I wanted to throw it out there.

Form fields have a way to get the "default value" (i.e. the value the field had when it was loaded), and you can compare that against the current value. A simple loop over all fields removes the need for maintenance if you add fields to the form.

There may or may not be various browser bugs associated with the "default value" properties, so I would not trust this method without extensive testing. The code below is a proof of concept, and is not used (by me) in any real application.

function IsDirty(form) {
    for (var i=0; i<form.elements.length; i++) {
        var field = form.elements[i];
        switch (field.type) {
            case "select-multiple":
            case "select-one":
                var options = field.options;
                for (var j=0; j<options.length; j++) {
                    if(options[j].selected != options[j].defaultSelected) return true;
                }
                break;
            case "text":
            case "file":
            case "password":
                if (field.value != field.defaultValue) return true;
                break;
            case "checkbox":
            case "radio":
                if (field.checked != field.defaultChecked) return true;
                break;
        }
    }
    return false;
}
Community
  • 1
  • 1
  • As far as I know this is the only way to do it. It prevents trickiness with, for example Firefox, retaining the new form values after a refresh. – Bob Jansen Dec 19 '11 at 09:04
9

Using jQuery this is very easy. You should be able to use the same premise to achieve the same result in vanilla javascript too.

var $inps = $('#myForm').find('input,select,textarea')
  , formAltered = false
;
$inps.change(function() {
    formAltered = true;
    $inps.unbind('change'); // saves this function running every time.
});

The only problem with this is if you change a value, and then change it back to the original, it'll still report the form as altered.

nickf
  • 537,072
  • 198
  • 649
  • 721
  • +1, You can elaborate this to eliminate the case of data not really changing - you can save the initial state and compare to it (use onfocus event for that) and only then set the form to "dirty" – Dror Mar 01 '09 at 07:50
  • You can make the first line better: `var $inps = $('#myForm :input'), formAltered = false;` – ANeves Apr 12 '12 at 20:37
6

Here is a one liner that you can add to your forms:

$(':input',document.myForm).bind("change", function() { 
  enablePrompt(true); }); // Prevent accidental navigation away

And then you can make the enableUnloadPrompt() function for your whole site:

function enablePrompt(enabled) {
  window.onbeforeunload = enabled ? "Your changes are not saved!" : null;
}

And finally, before you submit the form properly, make sure to:

enablePrompt(false);

This will not check to see if the form is different in values, only if the form was ever changed by the user. But, it is simple and easy-to-use.

jonstjohn
  • 59,650
  • 8
  • 43
  • 55
3

This could be handled with just one boolean variable, we call it dirty bit handling. If you have observed, generally in web pages, once user performs some edit action on any of the fields the form is considered as dirty(edited)(even if data remains unchanged after editing). When user tries to navigate away from the page user is prompted if he wants to save changes.

As per the standard practice, there is no check if after editing some field if the value actually got changed or not. For eg: If user edits and appends 'xyz' to a text field and then deletes 'xyz' essentially the form data remains the same as it was before but the form is still considered as 'dirty' and user is prompted warning message when he tries to navigate away.

So, if you want to implement this things get pretty simple. You would just need to add onchange() eventhandlers to the controls and set the global boolean variable something like isDirty to true inside those eventhandlers.

Once user wants to navigate away, you can flash a message "There may be unsaved changes on current page. Do you wish to save them?". User won't be disappointed even if he notices that his edit didn't change initial data.

Answers given above implement this very behavior. And I wrote this because you seemed to have an idea to check each and every field by it's initial value to see if it was really altered after edit. Just wanted to tell you that checking every field ain't necessary at all.

Real Red.
  • 4,991
  • 8
  • 32
  • 44
0

In my case I combined @Crescent Fresh and @nickf answers and got this one:

var formBefore;

var $inps = $('form').find('input,select,textarea');

$(function() {
    formBefore = $("form").serialize();
});

$inps.change(function () {
    var changedForm = $("form").serialize();
    if (formBefore != changedForm) {
        $("#btnSave").css('background-color', 'green');
    } else {
        $("#btnSave").css('background-color', '');
    }
});
Daniel Žeimo
  • 173
  • 4
  • 14
  • I think you'd better put this at 3rd line: `$("#btnSave").css('background-color', '');` – andymnc Nov 15 '22 at 17:10
  • I know it's years later, but still useful maybe. The color is different but the button's still working, so maybe better something like: `let formBefore; let $inps = $('form :input'); $("#btnSave").prop("disabled", true ); $(function() { formBefore = $("form").serialize(); }); $inps.change(function () { var changedForm = $("form").serialize(); if (formBefore != changedForm) { $("#btnSave").prop("disabled", false); } else { $("#btnSave").css('background-color', ''); $("#btnSave").prop("disabled", true ); } });` – andymnc Nov 15 '22 at 17:17
0

The answer posted earlier has proven itself (after some minor improvements) over the years. This is the function that I use in the onBeforeUnload event function.

/*
** Determines if a form is dirty by comparing the current
** value of each element with its default value.
**
** @param {Form} form the form to be checked.
** @return {Boolean} true if the form is dirty, false otherwise.
*/
function formIsDirty(form) {
    for (var i = 0; i < form.elements.length; i++) {
        var element = form.elements[i];
        var type = element.type;
        switch (element.type) {
        case "checkbox":
        case "radio":
            if (element.checked != element.defaultChecked)
                return true;
            break;
        case "number":
        case "hidden":
        case "password":
        case "date":
        case "text":
        case "textarea":
            if (element.value != element.defaultValue)
                return true;
            break;
        case "select-one":
        case "select-multiple":
            for (var j = 0; j < element.options.length; j++)
                if (element.options[j].selected != element.options[j].defaultSelected)
                    return true;
            break;
        }
    }
    return false;
}

Here is an example the onBeforeUnload handler function:

    function onBeforeUnload(event) {
        event = event || window.event;
        for (i = 0; i < document.forms.length; i++) {
            switch (document.forms[i].id) {
            case "search":
                break;
            default:
                if (formIsDirty(document.forms[i])) {
                    if (event)
                        event.returnValue = "You have unsaved changes.";
                    return "You have unsaved changes.";
                }
                break;
            }
        }
    }
Dandorid
  • 205
  • 2
  • 12
0

the <form> tag has the onChange callback that is triggered every time any of the fields triggers an onChange event.

Example:

<form onChange={isDirtyFunc}>
   <input name="name" type="text"/>
   <input name="address" type="text"/>
   <input name="age" type="number"/>
</form>

If any of the field changes the isDirtyFunc function is called with the relative event