Imagine the scenario: you have just completed entering your information into very long web form, and a notification pops up for something else. As you go to click the notification, some magical combination of your finger presses, mouse movement and silly computer behaviour causes the browser window to close, taking with it the last 30 minutes of effort. You scream in frustration, possibly break something and swear at the developer for not warning you about unsaved changes.

I’m sure at some point we have all experienced this situation, and the good news is you can try and avoid it in future for any web pages you work on. A demo of this plugin can be found here, and it works in all modern browsers. (even IE 8!)

The basis behind this functionality is the onbeforeunload event which is fired before the unload event. If the user closes the browser window or navigates away from your page, we can use setTimeout and some change tracking to detect if the page has unsaved edits and react accordingly.

The full implementation is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
var PM = PM || {};

/*
Object used to monitor if the page has unsaved changes and warn the user if the page does.
*/
(function ($) {
    PM.PageMonitor = function (options) {
        var self = this;

        var message = options.Message || "There are unsaved changes on this page. Are you sure you want to continue?";

        function getMessage() {
            if (typeof message === "function") {
                return message();
            }
            return message;
        }

        var userIsLeavingPage = false;

        var isDirty = options.IsDirty || function () { return false; };

        // Must be long enough for the unload handler to fire.
        var stayOnPageTimeoutValue = options.StayOnPageTimeout || 750;

        var noOp = function () { };

        var stayOnPageHandlerFunction = options.StayOnPageHandler || noOp;
        var unloadHandlerFunction = options.UnloadHandler || noOp;

        var forceRedirectFunction = options.ForceRedirect || function () { return false; };

        function stayOnPageHandler(event) {
            var that = this;
            if (!userIsLeavingPage) {
                stayOnPageHandlerFunction(event);
                stayOnPageTimeout = null;
            }
        };

        var stayOnPageTimeout = null;
        var outerTimeout = null;

        function onbeforeunloadHandler(event) {
            var that = this;
            var forceRedirect = forceRedirectFunction();
            if (isDirty() && !forceRedirect && stayOnPageTimeout === null) {

                // using an nested timeout due to this comment: http://stackoverflow.com/a/11835352/2090108
                outerTimeout = setTimeout(function () {
                    stayOnPageTimeout = setTimeout(function () {
                        stayOnPageHandler(event);
                    }, stayOnPageTimeoutValue); // Must be long enough for the unload handler to fire.
                }, 1);

                var msg = getMessage();
                return msg;
            }
            return;
        };

        $(window).on("beforeunload", onbeforeunloadHandler);

        function unloadHandler(event) {
            clearTimeout(outerTimeout);
            outerTimeout = null;
            clearTimeout(stayOnPageTimeout);
            stayOnPageTimeout = null;
            userIsLeavingPage = true;
            unloadHandlerFunction(event);
        };
        $(window).on("unload", unloadHandler);
    }
})(jQuery)

This example relies on jQuery to bind the event handlers, but if you prefer a cleaner implementation you could bind to event handlers on the window element directly.

An example of how to use the PageMonitor is shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
window.PageMonitor = PM.PageMonitor({
  Message: "WARNING!!! You have unsaved changes!!!",
  IsDirty: function () {
    return true; // How do you want to track the unsaved changes?
  },
  StayOnPageHandler: function (event) {
    // Do something if the user has stayed on the page...
  },
  ForceRedirect: function () {
    // You can get your value from anywhere here. This is useful for automatic redirects.
    return window.forceRedirect;
  }
});

The Message displayed to the user can be customised to a limited extent, depending on the browser used.

The IsDirty function checks if the page has any unsaved edits, using any method that is appropriate to your use case. The actual evaluation of the function is only triggered as the browser fires the onbeforeunload event.

Two other features worth mentioning are the StayOnPageHandler and the ForceRedirect functions.

One specific use case I have encountered is that when a user clicks on a link to navigate away, the page interaction is disabled and a “Loading” animation is shown, to indicate that the browser is busy, please wait…

However if the user wants to stay on the current page, the page interaction must be re-enabled and the “Loading” animation must be stopped. Hence the StayOnPageHandler can be called as appropriate.

The second useful feature I have used is the ability to force the page to redirect, even with unsaved edits. This could be due to the fact that the user is no longer authenticated and you need to redirect them automatically to the Login page. In this case the unsaved edits will be discarded anyway and you don’t want to give the user the choice of staying on the page.

Give the demo a bash and if you spot any bugs, or have any feature suggestions, please let me know.



This post was originally published on Entelect’s internal Tech Blog, Yoda.