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?
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. 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With