Warn user about unsaved changes before leaving the page

Warn Users in AdvanceAn Editing Session Handler for Navigating Away Alerts

Hey we all have gone thru the job of making sure a user doesn’t have pending modifications to be saved before navigating away from our page. If you haven’t, then I sure hope this helps you out.

The way I see this, there are 2 main scenarios in which we will want to do this:

  1. User has a form with updated data, and after changing the value of some <input> or <select>, the user tries to close the tab or window, or hits “Back” button on the browser
  2. The user has a form with updated data, but this form is really a “sub form” which is entirely AJAX submitted. Here, we want to warn the user that the subform has pending changes before he/she posts the form, closes the tab or window, or hits the “Back” button on the browser.

We’ll address the second scenario. Imagine an scenario where you want to edit a User profile, and part of the profile is an AJAX-editable list of preferences. The following is a Razor View file.

@model MvcApplication1.Models.Home.HomeViewModel

<h2>
    Index</h2>
@using (Html.BeginForm())
{
    <div>@Html.LabelFor(m => m.Username):</div>
    <div>@Html.TextBoxFor(m => m.Username)</div>
    
    <div>@Html.LabelFor(m => m.FullName):</div>
    <div>@Html.TextBoxFor(m => m.FullName)</div>
    
    int idx = 0;
    <h3>
        Favorite Numbers</h3>
    foreach (var favNumber in Model.FavoriteNumbers)
    {
        <div style="display: block;">@Html.Hidden("index", idx++)
            @favNumber
            <a href="#">Edit</a>
        </div>
    }
    
    <input type="submit" value="Submit" />
}

I’m not gonna post here a fully functional example cause that’s really not my intention (plus don’t have the time :)). So imagine that that Anchor tag you see there right next to the Favorite Number (favNumber) would:

  • Turn the “number” on the DOM into an <input type=”text” /> for editing.
  • Turn the Anchor itself into 2 Anchors (Save and Cancel).

That is all using Javascript (jQuery or whatever) of course. So now, what you want to do is handle the click on those “Save” links to do an AJAX post to the server to update the favorite number.

$("a.save").click(function () {

    $.post({ fav: $(this).parent().find("input").val() });
});

But what if the user:

  1. Loads the page for the User Profile
  2. Clicks on Edit to edit the first Favorite Number
  3. Updates the favorite number to “23”
  4. Clicks on “Submit”

That Submit will actually post the form with the Username and Fullname, but will NOT include the edited favorite number (actually it will, but not thru the AJAX feature you’ve envisioned).

So, an elegant way to handle this is to have a warning box appear to the user anytime the user tries to Submit a form (or navigate away from the page) with unsaved changes. I thought it would be useful to have a “stack” like feature that would allow the user to initiate as many edits as he/she wants, and warn when navigating away for as long as there is at least 1 pending edit to be commited (either saved or canceled).

So… here it is:

/*

* This global object will allow control over current pending modifications to be commited, and will
* warn the user whenever he/she tries to leave the page without committing them.
*
* Author: Mauricio Morales <mmorales@prosoftnearshore.com>
* This code is provided as-is, and we (Prosoft Nearshore) provide no support or warranty.
*/
UnsavedChangesWarning = new function () {
    /// Stores the current pending number of commits
    this.pendingCommits = 0;
    /// Stores a map of all callbacks
    this.callbacks = new Object();
    /// Pushes a new unsaved modification to be warned about. You should call this function when you
    /// start a modification that you want to warn the user about.
    /// The ID will uniquely identify your callback.
    this.push = function (id, callback) {
        this.pendingCommits++;
        if (id && callback) this.callbacks[id] = callback;
        // If this is the first change, then
        if (this.pendingCommits == 1) {
            window.onbeforeunload = function () {
                for (var key in UnsavedChangesWarning.callbacks) {
                    UnsavedChangesWarning.callbacks[key]();
                }
                return 'You have unsaved changes and will be lost! Are you sure you want to leave this page?';
            }
        }
    };
    /// Pops an old unsaved modification to be warned about.
    /// You should call this function when the user has either saved or canceled a modification.
    this.pop = function (id) {
        this.pendingCommits--;
        // if an ID is provided, then remove the callback
        if (id) delete this.callbacks[id];
        // if no more pending commits, then release the unsaved changes warning
        if (this.pendingCommits <= 0) {
            window.onbeforeunload = null;
        }
    };
};

What you’ll do is, simply, call that UnsavedChangesWarning.push(…) function when you start an AJAX editing mode/session. Then, call the UnsavedChangesWarning.pop(…) when the editing session is commited. This way, for as long as there is a push without a corresponding pop, the browser will alert the user when navigating away.

You can call that .push(…) function in 1 of 2 ways:

// this way, it'll just keep the push as a counter

UnsavedChangesWarning.push();
// this way, you can specify a callback to modify your DOM to somehow alert the
// user that the pending changes are <here>
UnsavedChangesWarning.push("mychanges", function () {
    $("input").css("color", "red").focus();
});

Now… correspondingly, the .pop() has 2 ways:

// this corresponds to the .push() call

UnsavedChangesWarning.pop();
// this corresponds to the .push(id, callback) call
UnsavedChangesWarning.pop("mychanges");

I won’t be held responsible for anything weird that may happen if you call a .push(id, callback) with a .pop(), or viceversa :).

What’s Good

  1. You can have several “editing sessions” all over your page, and the feature will handle them all nicely. And it even provides means of notifying the user of where are those open sessions.

What’s Bad

  1. It isn’t really that bad, but it won’t handle the first scenario for when we want the navigating-away warning (that’s, when the user modified some form <input>s and tried to hit back or close the browser.

What Could be Better

  1. It could somehow account and cover for that first scenario as mentioned above (in the What’s Bad first bullet).
  2. The name “UnsavedChangesWarning” isn’t really the best name for it, but I figured the feature was more important for the time being :).

Drop us a comment if you found this useful, or if you got feedback.

Acceptance Test-Driven Development

Acceptance Test DrivenIn our constant effort to continually improve our software processes, we’re beginning to explore Acceptance Test-Driven Development!    This would fit in with the current Agile model we employ and would help make sure we have more buy-in from the Product Owner in the planning process and make it easier for him/her at the same time.

Acceptance Test-Driven Development is a way to write the acceptance tests in advance and have the end user agree on the exact functionality before writing the code. Then the code can be written (and tested) without the need for extensive UAT (and bug fixes) after the fact.  Find out more about ATDD here.

There are many tools available such as Robot FrameworkCucumber (Ruby), FitNesse and Concordion.  We have begun to use FitNesse on a trial basis.  FitNesse is an integration testing tool that allows you to easily create and run automated tests and then verify that the code passes (or fails).  The important thing to note is that these tests apply to the business logic only – they do not test validations on text boxes and things like that.  But, you’ll know that your functionality is all there before you turn it over to UAT.  For more information about FitNesse go to www.fitnesse.org.

We would love to hear from you if you have worked with Fitnesse or any of the other tools out there and what your experience has been.