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

14

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 (14)

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?