jQuery Method To Prompt A User To Save Changes Before Leaving Page

Posted By : todd sharp Posted At : October 12, 2009 12:58 PM Posted In: jQuery

23

I'm doing a little work on cleaning up an application and came acrossed an undesirable circumstance related to the UI. The app contains what appears to be a tab navigator, but rather then the tabbed links revealing hidden form elements in another tab they take the user to a whole new page. The obvious problem with this design is that the user may populate elements in the first 'pseudo-tab' and click on the next tab assuming that they will be able to save the data after populating the other tab. Since I'm not at liberty to make massive changes to the UI I came up with the following solution using jQuery so the user is at least warned when they leave without saving their changes. I think it's a workable solution, and better yet it was terribly easy to do.

The first step here is to know when the form is 'dirty'. That is, when a user has made a change to the form that has not been saved. There are a number of ways to determine that a form is dirty, but I chose to declare a global variable called 'isDirty':

Note: Please see the update below for a better method using the onbeforeunload event of the window.

var isDirty = false;

The next step is to update the dirty flag when a user has made a change. When the DOM is ready I simply register a 'keyup' 'change' listener for all form inputs that will update the dirty flag as needed. Note: the ':input' selector will handle all form inputs, including <select> and <textarea> tags.

$(':input').change(function(){
    if(!isDirty){
        isDirty = true;
    }
});

Simple enough, right? Finally, I want to capture when the user clicks on a link that would navigate them away from the current page.

$('a').click(function(){
    if(isDirty){
        var confirmExit = confirm('Are you sure? You haven\'t saved your changes. Click OK to leave or Cancel to go back and save your changes.');
        if(confirmExit){
            return true;
        }
        else{
            return false;
        }
    }
});

Here we assign a click handler for every <a> element on the page. If the form is dirty we display a confirm dialog that warns them of the unsaved changes. If they confirm that they want to discard the changes we return true (allowing the normal action on the click event), else we return false (interrupting the normal link action). Since the pages save action will post the form there is no need to reset the dirty flag anywhere else, but if the save action was handled with Ajax you'd obviously want to reset the dirty flag.

I should mention that this solution is not fail proof. Any JavaScript methods that use 'window.location' to navigate will not be caught. Also, if the user closes the browser window they will not be prompted to save their changes. I may look into capturing the window unload event to handle that case, but memory tells me that the window unload event is pretty hard to catch in a reliable cross browser way.

To recap here is the entire example:

<script>
var isDirty = false;
$(document).ready(function(){
    $(':input').change(function(){
        if(!isDirty){
            isDirty = true;
        }
    });
    $('a').click(function(){
        if(isDirty){
            var confirmExit = confirm('Are you sure? You haven\'t saved your changes. Click OK to leave or Cancel to go back and save your changes.');
            if(confirmExit){
                return true;
            }
            else{
                return false;
            }
        }
    });
});
</script>

Update: It seems assigning a function to the window.onbeforeunload event works pretty well. Here is the simplified and revised function that should catch all attempts at leaving the page (closing the window, following links, etc):

var isDirty = false;
var msg = 'You haven\'t saved your changes.';

$(document).ready(function(){
    $(':input').change(function(){
        if(!isDirty){
            isDirty = true;
        }
    });
    
    window.onbeforeunload = function(){
        if(isDirty){
            return msg;
        }
    };
    
});

Comments (23)

johans's Gravatar What happens if the user changes something and then changes it back to the original - in other words no change was made?

You need to save the original form values and compare them before saving. I found this solution: http://code.google.com/p/jquery-form-observe/ that monitors the form and highlights fields that have been changed.

However check in the code as the poll interval is set to 1ms which really puts some load on slower CPUs - change it to 500ms or more.

todd sharp's Gravatar Interesting - nice find! In this particular application I think I'm comfortable with the occasional false positive that my method would report but I'll keep that in mind for future projects. Thanks for sharing!

Bard's Gravatar Why do you check if not already dirty before setting dirty?

Mike Lowry's Gravatar I don't think that the keyup event will capture changes in a select control if the user uses the mouse to change the value of the select. The change method seems to do the trick a little bit better. $(':input').change(function(){ //set flag })

todd sharp's Gravatar @Bard - if it's already dirty why set it again? Just to avoid unnecessary var setting.

@Mike - good catch. Let me test that out and I'll update the post.

todd sharp's Gravatar @Mike - actually the keyup technically works. There's no way you can change a form (that I can figure out) that won't fire a keyup. Even just tabbing to the select will fire the keyup.

Actually, the keyup is bad now that I think of it - because just tabbing thru fields will mark the form as dirty. I think I'll change it to the change event like you suggested.

I wish I had Flex's valueCommit event in JavaScript.

johans's Gravatar Something else you can check in the form observe code mentioned above is the use of window events - it alerts you of unsaved form if you try to shut the browser window.

Brian Swartzfager's Gravatar I recently wrote a jQuery plugin that tracks changes to form fields and denotes which ones are dirty. It's similar in concept to the one johans mentioned but it's designed to overcome the fact that you can't really "highlight" certain form elements like checkboxes and select boxes, and it lets you highlight the entire form if any field is in a changed state. You can check it out at http://www.thoughtdelimited.org/dirtyFields/

todd sharp's Gravatar Nice work Brian! I love jQuery and the community that uses it. So many (great) options when it comes to solving a problem!

Jaidev Soin's Gravatar Why not just make use of the serialize method?

On page load use $(yourForm).serialize();

On submit, re-serialize the form and see if it is any different, then handle that as required.

This is much shorter / more maintainable (you can add / remove fields from the form without having to touch this code).

Jaidev Soin's Gravatar Erm, not on submit sorry! I mean when the user leaves the page (when submit has not been clicked!)

n0nick's Gravatar Excellent write up, onbeforeunload() was just what I was looking for! :)

I'll just add that I found it useful to check the type of the target element so to avoid the pop-up when the user actually uses your form's submit button.

window.onbeforeunload = function(e)
{
   var _target = $(e.explicitOriginalTarget);
   if (window.is_dirty && _target.length && !_target.is(':input'))
   {
      return msg;
   }
};

Gary Gaetano's Gravatar Thanks for posting this! Just what I needed. I get requests for little scripts like this all the time from colleagues.

Debbie's Gravatar First ... EXCELLENT FIND!
I'm using the avoid popup on submit button part from n0nick dated jan 4. I cannot seem to get it to work in IE6 (yes IE6 .. our IT is planning upgrade to IE8 in a couple months). It works great in FF. I tried changing the IF statement to:
(window.isDirty && _target.length && !_target.is(':input'))
((window.isDirty || isDirty) && _target.length && !_target.is(':input'))
and a combo of is_dirty in there too and nothing triggers with this.

The original format works, however, I know I will get complaints if it pops up on submit also. Does anyone know how I can fix this so it works in IE6?

Tristan's Gravatar @Debbie - I found an issue with this method because of the info that Chrome puts in e doesn't reference the submit button so the .is(':input') never returns true; . I got around the issue by adding an event handler to the submit event of the form on the page, which clears the onbeforeunload event using:

window.onbeforeunload = null;

Hope this helps

Nathan's Gravatar Using the onbeforeunload approach from the end of your article, I worked around the "don't warn if you're actually submitting the form" issue by setting "isDirty" in the form's onSubmit() event.


$(':input').change(function(){isDirty = true;});

// BUT submitting the form should not prompt the warning.
$('#my_form').submit(function(){isDirty = false;});

window.onbeforeunload = function(){
if(isDirty){
return msg;
}
};

Jeff Riegel's Gravatar I modified this slightly to use a class instead of :input as the selector this allows me to select which fields I wish to monitor. I also modified the method to store the original values and check to insure they changed before displaying the message. I use the ResetDirty() function on an asp button that does a postback on save this way I don't see the message when I'm attempting to save it.

$(document).ready(function() {

$('.monitor').each(function(index) {
savedvalues[$(this).attr("id")] = $(this).val();
});

$('.monitor').change(function() {
if (!isDirty) {
isDirty = true;
}
});

window.onbeforeunload = function() {
if (isDirty) {
isDirty = false;
$('.monitor').each(function(index) {
if (savedvalues[$(this).attr("id")] != $(this).val())
isDirty = true;
});

if (isDirty)
return msg;
}
};

});

function ResetDirty(){
isDirty = false;
}

yigit karam's Gravatar God Bless You. Just what I needed.
and thank you Nathan for
$('#my_form').submit(function(){isDirty = false;});

Mike Henke's Gravatar Is there a way to customize the message even more? like the title, main body, and button verbiage?

I noticed there is slight differences between IE and FF.

Yasha's Gravatar thank you Nathan.

Russ's Gravatar Quick and Dirty, exactly what I needed. Thanks!

aleha's Gravatar Nice.
But got some troubles when you have validation on page, like unobstr... something. So then you click on submit and setting it to false, and no submit happened becouse there are validation errors on page. So isDirty in false and you are welcome to go avay without promt))
So i added some additional check in submit onClick event function:
if ($("Form").validate().form()) {
isDirty = false;
}
not nice but it works.

mark c's Gravatar tinyMCE :
thought I'd add this for cases where you have a tinyMCE textarea (tinyMCE converts textarea to iframe so is then not an input)

if (window.tinymce) {
tinymce.onAddEditor.add(function (sender, editor) {
   if(!isDirty){
    editor.onKeyUp.add(function (editor, event) {
    if (editor.isDirty())
    isDirty = true;
    });
    editor.onChange.add(function (editor) {
    if (editor.isDirty())
    isDirty = true;
    });
}
   })
};

however for reasons I don't understand the submit button check no longer works as isDirty seems to be reset by the tinyMCE action so I tweaked that, after adding a chkSub var:
$('#userform').submit(function(){
   isDirty = false;
   chkSub = true;
});

and amended this part of onbeforeunload function:
if(isDirty && !chkSub) {