AngularJS UI router $onLocationChangeStart event.PreventDefault does not work - angularjs

I need a way to interrupt a user navigating to a new page when there are unsaved changes on the current page. I implemented a modified version of the solution here:
http://weblogs.asp.net/dwahlin/cancelling-route-navigation-in-angularjs-controllers
However, what I see in the browser is that as soon as the user clicks on a link, the view changes and the new controller loads completely while the modal dialog is displayed. When the user clicks 'cancel' and event.preventDefault is fired, the user simply ends up on the new view. This is strange because everything I've read indicates that this is the accepted method, and nobody seems to have this issue. Yet I can't for the life of me see what is wrong with my code.
Here's the function in the main app for handling location changes (ModalService just wraps the angular bootstrap $modal service):
onRouteChangeOff = $rootScope.$on('$locationChangeStart', routeChange);
function routeChange(event, newUrl, oldUrl) {
//Navigate to newUrl if the form isn't dirty
if (!$rootScope.unsavedChanges) return;
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Ignore Changes',
headerText: 'Unsaved Changes',
bodyText: 'You have unsaved changes. Leave the page?'
};
ModalService.showModal({}, modalOptions).result.then(function () {
$rootScope.unsavedChanges = false;
$location.path(newUrl); //Go to page they're interested in
}
, function () {
event.preventDefault();
});
return;
}
Any ideas?

In case anyone else has this problem, the fix turned out to be quite simple. I moved the code to the stateChangeStart event. Code looks like this:
onRouteChangeOff = $rootScope.$on('$stateChangeStart', routeChange);
function routeChange(event, newState) {
//Navigate to newUrl if the form isn't dirty
if (!$rootScope.unsavedChanges) return;
event.preventDefault();
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Ignore Changes',
headerText: 'Unsaved Changes',
bodyText: 'You have unsaved changes. Leave the page?'
};
ModalService.showModal({}, modalOptions).result.then(function () {
$rootScope.unsavedChanges = false;
$state.go(newState); //Go to page they're interested in
});
}

Related

AngularJS confirmation modal custom action on No/Cancel button

https://weblogs.asp.net/dwahlin/building-an-angularjs-modal-service
I am using the above blog post as a guide to build the confirmation modal service in my Angular app. If I may ask, does anyone know how can the service be modified to support a custom action on the Cancel/No button in addition to dismissing the modal?
At the moment, the code only supports custom actions on the Yes button. I am having trouble understanding this part of the service code whereby I am not sure what the variable "result" actually is and how the functions can be modified to return something when the No/Cancel button is clicked:
$scope.modalOptions.ok = function (result) {
$modalInstance.close(result);
};
$scope.modalOptions.close = function (result) {
$modalInstance.dismiss('cancel');
};
Next is the code for invoking the modal from the controller.
var modalOptions = {
closeButtonText: 'Cancel',
actionButtonText: 'Yes',
headerText: 'Delete',
bodyText: 'Are you sure you want to delete this record?'
};
modalService.showModal({}, modalOptions)
.then(
function (result) {
//custom actions for Yes button here
}
);
In other words, when I invoke the modal in the controller, how do I intercept the flow after user clicks on the No/Cancel button on the modal?
I tried doing something like this and it didn't work.
modalService.showModal({}, modalOptions)
.then(
function (result) {
//custom actions for Yes button here
},
function (result) {
//custom actions for No button here
}
);

Angular ui-router Replace browser alert with ngSweetAlert on refresh/reload

I've read and searched for this and found variations (for example, on click) but not for the browser reload/refresh that works.
Basically, what I want is for when a user reloads, refreshes or F5 in the browser, instead of the regular alert confirm, the sweetalert dialog popups up asking the user if they want to refresh, losing whatever information they have viewed, with an OK/Cancel.
In my controller, I have this:
$scope.$on('$destroy', function() {
window.onbeforeunload = undefined;
});
window.onbeforeunload = function (e) {
e.preventDefault();
SweetAlert.swal({
title: "I display correctly but....",
text: "before page refreshes and do not wait for user to click ok",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#DD6B55",confirmButtonText: "Ok",
cancelButtonText: "Cancel",
closeOnConfirm: true,
closeOnCancel: true },
function(isConfirm){
if (isConfirm) {
console.log('do something...')
}
});
return undefined;
};
$scope.$on('$locationChangeStart', function( event, toState, toParams, fromState, fromParams) {
SweetAlert.swal({
title: "I display and wait for the user to click but too late",
text: "...after the page refreshes",
type: "warning",
showCancelButton: true,
confirmButtonColor: "#DD6B55",confirmButtonText: "Ok",
cancelButtonText: "Cancel",
closeOnConfirm: true,
closeOnCancel: true },
function(isConfirm){
if (isConfirm) {
console.log('do something...')
}
});
});
The "window.onbeforeunload" part loads at the right time (before the page reloads/refreshes) but does not wait for the user selection.
The "$scope.$on('$locationChangeStart',..." part loads too late (after the page has reloaded/refreshed) but does wait for the user selection.
Besides '$locationChangeStart,' I've also tried '$stateChangeStart' and '$routeChangeStart' to no avail.
What am I missing to make this work? Any help or direction would be greatly appreciated.
You should not override native functions such as alert() or confirm() because they are blocking and they are blocking for a good reason.
But if you really want to do it, it is possible like this
/**
* Definition of global attached to window properties <br/>
*/
(function() {
nalert = window.alert;
Type = {
native: 'native',
custom: 'custom'
};
})();
/**
* Factory method for calling alert().
* It will be call a native alert() or a custom redefined alert() by a Type param.
* This defeinition need for IE
*/
(function(proxy) {
proxy.alert = function () {
var message = (!arguments[0]) ? 'null': arguments[0];
var type = (!arguments[1]) ? '': arguments[1];
if(type && type == 'native') {
nalert(message);
}
else {
// TODO: Call to SweetAlert()
document.write('<h1>I am redefiend alert()<br/>Alert say: '+message+'</h1>');
}
};
})(this);
More on this here
JavaScript: Overriding alert()

$scope.$on('$stateChangeStart') and $modal dialog

I have an AngularJs application that is detecting the change of the state (using ui.router) to present the user with the option to save unsaved changes. Now I'm doing this with a confirm dialog:
$scope.$on('$stateChangeStart', () => {
if (self.changed && confirm('There are unsaved changes. Do you want to save them?'))
this.save();
});
I wanted to change it to using the $modal dialog from the bootstrap ui library. The problem I have is that as the $modal.open() call returns inmediatelly (being asynchronous) the state changes before opening the dialog and it's never opened.
$scope.$on('$stateChangeStart', () => {
if (self.changed)
this.$dialog.open({...}).result.then(()=>{
this.save();
});
});
Is there a way to overcome this problem or am I stuck to using the plain javascript confirm dialog?
This is how I solved the problem. In my application, I am using an AppCtrl (parent) to handle navigation, dirty state etc.
function AppCtrl($rootScope, events, modalDialog) {
var vm = this,
handlingUnsavedChanges = false;
function isDirty() {
return $rootScope.$broadcast(events.CAN_DEACTIVATE).defaultPrevented;
}
function onStateChangeStart(event, toState, toParams) {
if (handlingUnsavedChanges) {
// if the dirty state has already been checked then continue with the state change
return;
}
// check for dirty state
if (isDirty()) {
// cancel navigation
event.preventDefault();
modalDialog
.confirmNavigation()
.then(function () {
// ignore changes
handlingUnsavedChanges = true;
$state.go(toState.name, toParams);
});
}
// Else let the state change
}
$rootScope.$on('$stateChangeStart', onStateChangeStart);
}
<div ng-controller="AppCtrl as app">
<div ui-view />
</div>
Then you can add an event handler for CAN_DEACTIVATE event in your route controller to check for dirty state, for example
function UserDetailCtrl($scope, events) {
function isDirty() {
// Your logic, return a boolean
}
function canDeactivate(e) {
if (isDirty()) {
e.preventDefault();
}
}
$scope.$on(events.CAN_DEACTIVATE, canDeactivate);
}
You can do it perfectly fine with ui-router and settings in run section of your app.
The crucial part is basically watching the $rootScope.$on('$stateChangeStart') event.
I present the whole solution with the plunker: http://plnkr.co/edit/RRWvvy?p=preview The main part is in scipt.js, and it goes as follows:
routerApp.run(function($rootScope, $uibModal, $state) {
$rootScope.modal = false; // this has to be set so the modal is not loaded constantly, and skips the 1st application entrance
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams) {
if ($rootScope.modal) {
event.preventDefault(); //this stops the transition
$rootScope.modal = false;
var modalInstance = $uibModal.open({
templateUrl: 'modal.html'
});
modalInstance.result.then(function(selectedItem) {
console.log('changing state to:'+ toState.name);
$state.go(toState, {}, {reload:true});
}, function() {
console.log('going back to state:'+ fromState.name);
$state.go(fromState, {}, {reload:true});
});
} else {
$rootScope.modal = true;
}
});
});
You should listen the $locationChangeStart event and if you have unsaved changes then do event.prventDefault() and continue on with your logic for the confirmation dialog with ui-bootstrap modal. The thing is that the order of the events is slightly changed in the newer angular versions and the $stateChangeStart happens a little too late to do your logic there. If you get stuck I can provide you with working example. For now, here is sample code:
$scope.$on('$locationChangeStart', () => {
if (self.changed) {
event.preventDefault();
this.$dialog.open({...}).result.then(()=>{
this.save();
// your logic for the redirection if needed after the save
});
}
});
If you want to work with state change event , it would be better to apply the event on app and use $modal service. You can find a detailed explanation of this here
http://brewhouse.io/blog/2014/12/09/authentication-made-simple-in-single-page-angularjs-applications.html
However this gives an example that works for each state change whenever state changes but you can make it working for the single or specified states like this :
app.run(function ($rootScope) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if(toState.name =="desiredState") {
event.preventDefault();
// perform your task here
}
});
});
Since I'm not very comfortable with using broadcast I used below approach.
I simply cancel current event (state change) before showing my JQuery Dialog. And if user selects 'yes', then i trigger $state.go, if selects 'cancel/no'- we don't need to do anything, we already cancelled event.
$rootScope.$on('$stateChangeStart',function (event, toState, toParams, fromState, fromParams, options) {
console.log('$stateChangeStart- fromState= '+fromState.name +'-toState= '+toState.name);
/*If user starts to change state, show him a confirmation dialog
* If user selects 'Yes' - continue with state change and go in pause mode for collection.
* If user selects 'No'- stop state change.
*/
if (/*some condition*/) {
/*stateChangeStart gets triggered again by $state.go initiated from our modal dialog-
hence avoid extra cycle of listening to stateChangeStart*/
if (service.stateChangeTriggeredByDialog) {
service.stateChangeTriggeredByDialog = false;
return;
}
if (fromParams.paramName !== toParams.paramName) {
event.preventDefault();
Dialog.confirm({
dialogClass: 'my-customDialog',
template: $translate.instant('STATE_CHANGE_MSG'),
resizable: false,
width: 550
}).then(function () {
console.log('User choose to continue state change');
service.stateChangeTriggeredByDialog = true;
$state.go(toState, toParams, { inherit: false });
}, function () {
console.log('User choose to stop state change');
});
}
}});

How to prevent default browser back button functionality in AngularJS?

I'm making a mobile app and it has a back button in the navbar of the app. The button is used for navigating one level up in the application. Not all levels of the applications are written in the url, some of them are skipped so i cannot rely on the browser back button because not everything is written in the history. So what i want to do is to prevent the default event that happens when you click the back button (even the url change because i have a function that's manually rewriting the url when you click on some parts of the app) and i want the browser's back button to be tied to my custom function.
I'm thinking of adding a false state in the history with history.pushstate so when i'm pressing back i'll be going to the false state, but unfortunately you can't have two states with a same name.
Do you guys know any solution for this?
Thanks
We want to prevent that users can use the back button when logged off or when the token is not valid anymore.
// Prevent to use the back button.
$scope.$on('$locationChangeStart', function(event) {
if (!$scope.isAuthenticated) {
event.preventDefault();
}
});
Where the if is you can implement your url rewrite function.
Hope this helps
try this:
$rootScope.$on("$routeChangeStart", function(event, next, current) {
if(current && current.params === "example") {
$location.path("/defaultPage");
}
});
The following code should do the trick:
var allowNav = false;
var checkNav = false;
$rootScope.$on('$stateChangeSuccess', function (event, toState, toStateParams, fromState, fromStateParams) {
allowNav = checkNav;
checkNav = true;
});
$rootScope.$on('$locationChangeStart', function (event, next, current) {
// Prevent the browser default action (Going back)
if (checkNav) {
if (!allowNav) {
event.preventDefault();
}
else {
allowNav = false;
}
}
});

How to cancel routeChange or browser reload when form is incomplete?

There are scenarios like:
Browser reload,
Closing tab
closing browser
Route change (e.g. clicking on links)
Browsers back button was clicked. or history.go(-1). 3 fingers swipe on Macbooks.
that we would want to prevent if the user has filled some sort of form or is in middle of the writing.
I have written this code which works fine but its absolutely not useful if I cant implement it on several textfields. Currently it only check if we are at #/write url. It doesnt check any inputs.
Whats the angular way to deal with this? Whats the best way to check the target textfield. Is a directive the solution?
something like:
<input type="text" warningOnLeave ng-model="title"/>
or
<form warningOnLeave name="myForm">...</form>
$rootScope.$on('$locationChangeStart', function(event, current, previous){
console.log(current);
console.log(previous);
// Prevent route change behaviour
if(previous == 'http://localhost/#/write' && current != previous){
var answer = confirm ("You have not saved your text yet. Are you sure you want to leave?");
if (!answer)
event.preventDefault();
}
});
/**
Prevent browser behaviour
*/
window.onbeforeunload = function (e) {
if(document.URL == 'http://localhost/#/write'){
e = e || window.event;
// For IE and Firefox prior to version 4
if (e) {
e.returnValue = 'You have not saved your text yet.';
}
// For Safari
return 'You have not saved your text yet.';
}
else
return;
}
Forms in Angular have the $dirty/$pristine properties that mark if the user has/hasn't interacted with the form controlls, and the accompanying method $setPristine(). I would base the desired functionality on this feature. Consider:
<form name="theForm" ng-controller="TheCtrl" ...>
This puts the form in the scope of the controller, under the given name. Then something like:
controller("TheCtrl", function($scope, $rootScope) {
$rootScope.$on('$locationChangeStart', function(event, current, previous) {
if( $scope.theForm.$dirty ) {
// here goes the warning logic
}
});
});
Do not forget to call $scope.theForm.$setPristine() where appropriate (i.e. after submitted or cleared).
For the window unload case, you will have to watch the $dirty flag. So in the previous controller:
$scope.$watch("theForm.$dirty", function(newval) {
window.myGlobalDirtyFlag = newval;
});
You have to do this because the window.onbeforeunload event does not have access to the scope of the form. Then, in the global section of your app:
window.onbeforeunload = function (e) {
if( window.myGlobalDirtyFlag === true ) {
// warning logic here
}
};
Again, you may want to clear the global dirty flag when the scope is destroyed, so in the controller:
$scope.$on("$destroy", function() {
window.myGlobalDirtyFlag = false;
});

Resources