Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent user to navigate away with unsaved changes

I'm currently using Backbone.Marionette to create a SPA and in one of the views it is possible for the user to navigate away with unsaved changes. I can control some of these events, like buttons and menu options within the view that would take the user away, but some others would require to manipulate either Backbone.Router or work with the DOM events directly.

I already tried listening to beforeunload (doesn't work as the application is still loaded) and hashchange (doesn't work as you cannot stop the browser from navigating away). These solutions (1, 2, 3) don't work in this case, the Javascript is never unloaded.

Changing the Backbone.Router seems to be the best option, but because of how it is initialized I don't think it is possible to introduce this feature or at least I cannot find a way of doing it. This solution, for example, doesn't work because hashchange is not cancelable (you cannot call stopPropagation on it), and this other solution doesn't work because navigate is not defined on the Backbone.Router object.

Any suggestions?

like image 840
JayC Avatar asked Nov 10 '22 02:11

JayC


1 Answers

I've managed to find a solution to this, although some more work is required. For this solution, I am assuming that you keep track when a view is dirty.

There are 4 main ways of moving out of a view;

  1. Click on a link on the view
  2. Click on link outside the view
  3. Click on refresh or external link
  4. Click on back/forward on the browser

1. Application link

This is the easiest case. When you click on your own link, you have to check if your view is dirty. For example, I have an in-app back button that is handled by a historyBack function. On the view:

historyBack: function() {
    if (this.isDirty) {
         answer = confirm("There are unsaved changes.\n\nDo you wish to continue?")

        if (answer) {
            this.isDirty = false
            window.history.back()
        }
    }

    else {
        window.history.back()
    }
}

2. Links outside your view

This type of interaction can be handled by extending the Router prototype's execute method, not the navigate method as proposed in other places.

There should be a variable somewhere accessible by the Router that stores the state of the view. In my case, I'm using the Router itself and I update this variable every time I change the dirty flag on the view.

The code should look something like this:

_.extend(Backbone.Router.prototype, {

    execute: function (callback, args, name) {
        if (Backbone.Router.isDirty) {
            answer = confirm "There are unsaved changes.\n\nDo you wish to continue?";

            if (!answer) {
                return false;
            }
        }

        Backbone.Router.isDirty = false
        if (callback) callback.apply(this, args) 
    }
}

3. Refresh or external link

Refresh and external links actually unload your Javascript so here the solutions based on beforeunload (see question) actually work. Wherever you manage your view, I use a controller but let's assume it's on the same view, you add a listener on show and remove it on destroy:

onShow: function() {
    $(window).bind("beforeunload", function (e) {
        if (this.isDirty) {
             return "There are unsaved changes.";
        }
    }
}

onDestroy: function() {
    $(window).unbind("beforeunload");
}

4. Back/Forward on the browser

This is the trickiest case and the one I haven't figured out completely yet. When hitting back/forward, the user can navigate out of the app or within the app, both cases are covered by the code on 1 and 3, but there is an issue I can't figure out and I will create another question for it.

When hitting back/forward, the browser changes the address bar before calling the router so you end up with an inconsistent state: The address bar shows a different route to the application state. This is a big issue, if the user clicks again on the back button, after saving or discarding the changes, she will be taken to another route, not the previous one.

Everything else works fine, it shows a pop up asking the user if she wants to leave or continue and doesn't reload the view if the user chooses to stay.

like image 162
JayC Avatar answered Nov 15 '22 10:11

JayC