Prevent Navigating To Another View If Contents Are Unsaved
Solution 1:
I would avoid hacking around with Backbone. You could do this globally for all links by replacing the part where you would normally start Backbone.history
with something like
initRouter: function () {
Backbone.history.start({ pushState: true });
$(document).on('click', 'a', function (ev) {
var href = $(this).attr('href');
ev.preventDefault();
if (changesAreSaved) {
router.navigate(href, true);
}
});
}
You need of course to replace the changesAreSaved
with something that makes sense and add whatever other login you have about handling links.
Solution 2:
I would also hack Backbone.history.loadUrl
, that's where loading the route callbacks happen.
// ALLOW PREVENTING HASH NAVIGATIONvar originalFn = Backbone.history.loadUrl;
Backbone.history.loadUrl = function() {
// I introduced an application state variable, but it can be solved in multiple waysif (app && app.states.isNavigationBlocked) {
var previousFragment = Backbone.history.fragment;
window.location.hash = '#' + previousFragment;
returnfalse;
}
else {
return originalFn.apply(this, arguments);
}
};
Explanation:
1)
Backbone listens to the hashchange
event and sets Backbone.history.checkUrl
as a callback:
https://github.com/jashkenas/backbone/blob/1.1.2/backbone.js#L1414
Backbone.$(window).on('hashchange', this.checkUrl);
2)
Backbone.history.checkUrl
checks if the hash has changed and calls Backbone.history.loadUrl
checkUrl: function(e) {
var current = this.getFragment();
if (current === this.fragment && this.iframe) {
current = this.getFragment(this.getHash(this.iframe));
}
if (current === this.fragment) returnfalse;
if (this.iframe) this.navigate(current);
this.loadUrl();
},
3)
Backbone.history.loadUrl
finds the first matching route and calls its callback:
loadUrl: function(fragment) {
fragment = this.fragment = this.getFragment(fragment);
return _.any(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
returntrue;
}
});
},
Useful note:
Backbone.history.fragment
stores the current hash, it's set in Backbone.history.loadUrl
, so we can access it AFTER the hashchange
event but BEFORE the router callbacks do their job.
Solution 3:
I think you could hack Backbone.history.loadUrl ( http://documentcloud.github.com/backbone/docs/backbone.html#section-137 ). I did a quick test, this code does a check every time you change pages. You'll want to add code to activate the check only when there is actually a reason for it.
var goingBack = false;
functiondoCheck() {
// TODO: add code that checks the app state that we have unsaved datareturn goingBack || window.confirm("Are you sure you want to change pages?");
}
var oldLoad = Backbone.History.prototype.loadUrl;
Backbone.History.prototype.loadUrl = function() {
if(doCheck()) {
return oldLoad.apply(this, arguments);
} else {
// change hash back
goingBack = true;
history.back();
goingBack = false;
returntrue;
}
}
You'll have to handle window.onbeforeunload as well, because the user might still leave the page entirely.
Solution 4:
Since version 1.2.0 you can override method Router.execute
and return false
to cancel routing, like this:
execute: function(callback, args, name) {
if (!changesAreSaved) {
// tip: .confirm returns false if "cancel" pressedreturnwindow.confirm("You sure have some unsaved "
+ "work here, you want to abandon it?");
}
// this is the default part of "execute" - running the router actionif (callback)
callback.apply(this, args);
}
Solution 5:
I've been dealing with this issue for a little while, and I've come up with a solution. I based my solution off this example.
The idea is to override the navigate
method, and use jQuery
deferred
objects to wait to the appropriate time to navigate. In my case, if a user tried to navigate from my view that was dirty, a dialog needed to show that asked the user if:
1) Save the changes, then navigate 2) Don't save the changes, and navigate 3) Cancel the navigation and remain on the existing page
Below is my code for the navigate method in the Router
:
navigate: function(fragment, trigger) {
var answer,
_this = this;
answer = $.Deferred();
answer.promise().then(function() {
returnBackbone.Router.prototype.navigate(fragment, trigger);
});
if(fragment !== undefined){
var splitRoute = fragment.split('/');
app.currentPatronSection = splitRoute[splitRoute.length - 1];
}
if (app.recordChanged) {
this.showConfirm(function(ans){
// Clear out the currentView
app.currentView = undefined;
answer.resolve();
}, function(){
});
return answer.promise();
} else {
return answer.resolve();
}
returnBackbone.Router.prototype.navigate(fragment, trigger);
},
The showConfirm
method presents the dialog with the three options I listed above. Based on the user's choice, I save the form, then resolve the answer to navigate, etc.
Post a Comment for "Prevent Navigating To Another View If Contents Are Unsaved"