Client/JS Framework for "Unsaved Data" Protection?

One piece of the puzzle:

/**
 * 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} <code>true</code> if the form is dirty, <code>false</code>
 *                   otherwise.
 */
function formIsDirty(form)
{
    for (var i = 0; i < form.elements.length; i++)
    {
        var element = form.elements[i];
        var type = element.type;
        if (type == "checkbox" || type == "radio")
        {
            if (element.checked != element.defaultChecked)
            {
                return true;
            }
        }
        else if (type == "hidden" || type == "password" || type == "text" ||
                 type == "textarea")
        {
            if (element.value != element.defaultValue)
            {
                return true;
            }
        }
        else if (type == "select-one" || type == "select-multiple")
        {
            for (var j = 0; j < element.options.length; j++)
            {
                if (element.options[j].selected !=
                    element.options[j].defaultSelected)
                {
                    return true;
                }
            }
        }
    }
    return false;
}

And another:

window.onbeforeunload = function(e)
{
    e = e || window.event;  
    if (formIsDirty(document.forms["someFormOfInterest"]))
    {
        // For IE and Firefox
        if (e)
        {
            e.returnValue = "You have unsaved changes.";
        }
        // For Safari
        return "You have unsaved changes.";
    }
};

Wrap it all up, and what do you get?

var confirmExitIfModified = (function()
{
    function formIsDirty(form)
    {
        // ...as above
    }

    return function(form, message)
    {
        window.onbeforeunload = function(e)
        {
            e = e || window.event;
            if (formIsDirty(document.forms[form]))
            {
                // For IE and Firefox
                if (e)
                {
                    e.returnValue = message;
                }
                // For Safari
                return message;
            }
        };
    };
})();

confirmExitIfModified("someForm", "You have unsaved changes.");

You'll probably also want to change the registration of the beforeunload event handler to use LIBRARY_OF_CHOICE's event registration.


If you use jQuery, here's an easy trick:

$('input:text,input:checkbox,input:radio,textarea,select').one('change',function() {
  $('BODY').attr('onbeforeunload',"return 'Leaving this page will cause any unsaved data to be lost.';");
});

But just remember, if you have a condition where you redirect from this page, or you want to permit a successful form post, you need to do this before that redirect or submit event like so:

$('BODY').removeAttr('onbeforeunload');

...or you'll get yourself in a loop where it keeps asking you the prompt.

In my case, I had a big app and I was doing location.href redirects in Javascript, as well as form posting, and then some AJAX submits that then come back with a success response inline in the page. In any of those conditions, I had to capture that event and use the removeAttr() call.


Wanted to expand slightly on Volomike excellent jQuery code.

So with this, we have a very very cool and elegant mechanism to accomplish the objective of preventing inadvertent data loss through navigating away from updated data prior to saving – ie. updated field on a page, then click on a button, link or even the back button in the browser before clicking the Save button.

The only thing you need to do is add a “noWarn” class tag to all controls ( especially Save buttons ) that do a post back to the website, that either save or do not remove any updated data.

If the control causes the page to lose data, ie. navigates to the next page or clears the data – you do not need to do anything, as the scripts will automatically show the warning message.

Awesome! Well done Volomike!

Simply have the jQuery code as follows:

$(document).ready(function() {

    //----------------------------------------------------------------------
    // Don't allow us to navigate away from a page on which we're changed
    //  values on any control without a warning message.  Need to class our 
    //  save buttons, links, etc so they can do a save without the message - 
    //  ie. CssClass="noWarn"
    //----------------------------------------------------------------------
    $('input:text,input:checkbox,input:radio,textarea,select').one('change', function() {
        $('BODY').attr('onbeforeunload',
        "return 'Leaving this page will cause any unsaved data to be lost.';");
    });

    $('.noWarn').click(function() { $('BODY').removeAttr('onbeforeunload'); });

});

Additional to Lance's answer, I just spent an afternoon trying to get this snippet running. Firstly, jquery 1.4 seems to have bugs with binding the change event (as of Feb '10). jQuery 1.3 is OK. Secondly, I can't get jquery to bind the onbeforeunload/beforeunload (I suspect IE7, which I'm using). I've tried different selectors, ("body"), (window). I've tried '.bind', '.attr'. Reverting to pure js worked (I also saw a few similar posts on SO about this problem):

$(document).ready(function() {
    $(":input").one("change", function() {
        window.onbeforeunload = function() { return 'You will lose data changes.'; }
    });
    $('.noWarn').click(function() { window.onbeforeunload = null; });
});

Note I've also used the ':input' selector rather than enumerating all the input types. Strictly overkill, but I thought it was cool :-)