Back button confirm doesn't work in AngularJS app - angularjs

In one page of my AngularJS app I want to display a confirm if the user click on the browser back button :
So I added a listener on stateChange to detect this behavior, and if the user don't want to go back I cancel it.
Here is my piece of code :
angular
.module('app.platform')
.run(start);
start.$inject = ['$rootScope', '$state'];
function start ($rootScope, $state) {
// Save navigation
$rootScope.previousState = null;
// State change listener
$rootScope.$on('$stateChangeStart', stateChangeStartListener);
function stateChangeStartListener(event, toState, toParams, fromState, fromParams) {
// app.platform.quiz is the state that I want to display a confirm if the user
// uses the back button
// Then I record and check the previous state reached to know that the user uses the back button
if(fromState.name === 'app.platform.quiz'
&& $rootScope.previousState
&& $rootScope.previousState === toState.name) {
if(confirm("Do you really want ot leave the quiz ?")) {
$rootScope.previousState = fromState.name;
$state.go(toState, toParams);
} else {
event.preventDefault();
}
} else {
$rootScope.previousState = fromState.name;
}
}
}
When the user hits OK button it works well, but when he hits Cancel I got a strange behaviour. He stays on the page, but if he hits the back button again nothing happen (no confirm, no go back), and if he hits back button again he go back 2 states before.
It sounds like my event.preventDefault() mess with the history.
I'm not very familiar with the HTML5 History API, but maybe it can solve my problem.
Do you have any idea what I'm doing wrong ? Or why I got this behaviour (and how to correct it).
EDIT :
After some reading (from comments), I know what is the problem, and how to solve it (only in theory).
I need to catch the back button click before it change the URL, but I think it's not possible.

Related

AngularJS UI routing $stateChangeStart dont work as expected

I have created an app with angular ui routing and states. Also I have a WEB API controller that can use to learn if user is authenticated or not. The problem is that http request to that WEB API has some delay to return result because is asynchronous.
I want, every time when user wants to go on state, to check if is authenticated or not, and give him access or redirect to login page. But first time when app running with someway i want to wait until i have answer from my WEB API.
I have this part of code
PlatformWeb.run(function ($rootScope, $state, $timeout, Authorization) {
$rootScope.$state = $state;
$rootScope.User = {
isAuthenticated: null,
targetState: null
}
Authorization.isAuthenticated().then(function (result) {
$rootScope.User.isAuthenticated = result;
$state.go($rootScope.User.targetState.name);
})
hasAccess = function () {
return $rootScope.User.isAuthenticated;
}
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
$rootScope.User.targetState = toState;
if ($rootScope.User.isAuthenticated == null)
event.preventDefault();
else {
if (!hasAccess()) {
event.preventDefault();
$state.go('PlatformWeb.Login');
}
}
});});
The first time when app runs, $rootScope.User.isAuthenticated is null, so i will prevent to load state. Also with my 'Authorization' service i call my asynchronous function to get if user if authenticated or not. Before i prevent loading state i keep when user wants to go, so when i have my result back from WEP API, I change the state '$state.go($rootScope.User.targetState.name);' to the state he want. Then I know if is authenticated or not and i ask if has permissions to go. If he hasn't then i redirect him to login state.
BUT event.preventDefault(); doesn't work as expected. When i run my app i get this error 'angular.min.js:6 Uncaught Error: [$rootScope:infdig] http://errors.angularjs.org/1.3.7/$rootScope/infdig?p0=10&p1=%5B%5D' multiple times.
Angular documentation says :
'10 $digest() iterations reached. Aborting!
Watchers fired in the last 5 iterations: []'
In my logic, 'stateChangeStart' function in condition '$rootScope.User.isAuthenticated == null' with event.preventDefault(); will make app logs here, and not run more until i know. When i get my result back from my WEBAPI I will go again in this function, but this time i know in which state to send him.
I would suggest, firstly always check where is current change navigating to... and get out if it is already redirected
$rootScope.$on('$stateChangeStart',
function (event, toState, toParams, fromState, fromParams) {
// are we already going to redirection target?
if(toState.name === 'PlatformWeb.Login'){
return; // yes, so do not execute the rest again...
}
$rootScope.User.targetState = toState;
if ($rootScope.User.isAuthenticated == null)
event.preventDefault();
else {
if (!hasAccess()) {
event.preventDefault();
// we will redirect here only if not already on that way...
$state.go('PlatformWeb.Login');
...

Detect ui-router state change from within controller and close Modal accordingly

I open a UI-Bootstrap Modal using the $modal.open({...}) method. I need this to close when the user presses the back button.
The result promise returned by the open() method is not useful in this case as it cannot detect the state change due to the back button. Right now when the back button is pressed, the state changes but the modal stays open.
Basically I am having the exact problem as this question but even though it has a selected answer, the problem is not solved as evidenced from the comments. This other question is similar but also doesn't solve the back button issue.
I need some way to detect that the current state has changed from within the controller and call $modalInstance.close() or the $scope.$close() methods.
I could listen for $stateChangeStart event and check the fromState argument to conditionally close the modal. But then this event would unnecessarily keep firing for all subsequent state changes too.
UPDATE: So I tried listening for the event and deregistered it as soon as it is fired for the first time. This way I get to listen for the back button state change and then stop it when I want. The final code for the modal state is as follows:
$stateProvider.state('itemList.itemNew', {
url: '/new',
onEnter: function($state, $modal) {
$modal.open({
templateUrl: "/static/partials/item/form.html",
controller: function($http, $scope, $modalInstance) {
$scope.editableItem = {};
$scope.saveItem = function(item) {
$http.post('/api/item', item)
.success(function(data) {
$modalInstance.close(data);
alert("Saved Successfully");
}).error(function(data) {
alert("There was an error.");
});
};
//Register listener specifically for the back button :(
deRegister = $scope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams) {
if (toState.name === 'itemList' &&
fromState.name === 'itemList.itemNew') {
$modalInstance.close();//Close the modal
deRegister();//deRegister listener on first call
}
}
);
}
}).result.then(function() {
//Promise Resolved, Modal Closed.. So reload
$state.go("^", null, {
"reload": true
});
}, function() {
//Promise Rejected, Modal Dismissed.. no reload
$state.go("^");
});
},
});
I still think there should be a better way to do it. Constellates apparently decided to dump modal.js from ui-bootstrap altogether. Should I do the same and simply render the modal using plain Bootstrap CSS out of a <ui-view/>?
I needed to address the same issue. Maybe a slightly different setup, but I believe it may work for you.
I am using the angular-modal-service, which I believe is running on bootstrap anyway.
Inside the controller for the modal I used the following:
$scope.$on('$stateChangeStart', function() {
$scope.stateChange();
});
$scope.stateChange = function() {
// Manually hide the modal using bootstrap.
$element.modal('hide');
// Now close as normal, but give 500ms for bootstrap to animate
close('', 500);
};
The second function is just the manual way of exiting the modal as per the docs under "I don't want to use the 'data-dismiss' attribute on a button, how can I close a modal manually?". It says:
All you need to do is grab the modal element in your controller, then call the bootstrap modal function to manually close the modal. Then call the close function as normal
So now, if a state change happens (including a user-initiated back-button click), the modal gracefully zips away.

ui-router resume preventedEvent

I want users to be notified that they have to save changes when they navigate to another page.
Therefore I listen to the stateChangeStart Event of UI-ROUTER.
I need to prevent the event to prevent, that the next page doesn't load the values before they are saved. When I manually navigate to the desired page via $state.go(..) I keep hanging in a loop of stateChangeStart Events
$scope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
event.preventDefault();
var confirmPopup = $ionicPopup.confirm({
....
});
confirmPopup.then(function(res){
....
}
$ionicNavBarDelegate.back();
});
});
How to log of the stateChangeStart-Event?
Edit:
$urlRouter.sync();
$state.go(toState.name, {id:toParams.id}, {reload: false});
Stays inside the loop.
$urlRouter.sync()
Returns null.
Triggers an update; the same update that happens when the address bar
url changes, aka $locationChangeSuccess. This method is useful when
you need to use preventDefault() on the $locationChangeSuccess event,
perform some custom logic (route protection, auth, config,
redirection, etc) and then finally proceed with the transition by
calling $urlRouter.sync().
See docs.
The use of the urlRouter.sync() is pretty obvious if you look at the docs, if you want to go to another state or want to check something in between the transition, you could also do this to prevent a redirect loop:
var nextState = null;
$rootScope.$on('$stateChangeStart', function (event, next, current) {
if (next.name == nextState) return;
event.preventDefault();
nextState = next.name;
$state.go(next.name);
});

$location.path not navigating propertly within $locationChangeStart callback

In order to prevent view changes on a form with edits, I am using $locationChangeStart to intercept the view change. In that function, I am attempting to use a bootstrap dialog to prompt the user.
It all works ok, until the part where I call $location.path(current) to change the view. Instead of navigation to the appropriate route, it goes to the default route (the home page). Why is this?
Here is the code I am using in my controller:
function onNavigate() {
var turnOff = $scope.$on('$locationChangeStart',
function (event, current, previous) {
if (vm.canSave) {
dialogService.confirmationDialog('Are you sure?', 'You have unsaved changes, are you sure you want to abandon your form?')
.then(function() {
turnOff();
$location.path(current);
});
event.preventDefault();
});
}
In the debugger, the value of current is something like
http://localhost:3091/#/participant/24
at the $location.path line, however my application ends up at
http://localhost:3091/#/

AngularJS window.onbeforeunload in one controller is being triggered on another controller

Here is my problem, I have two views (View1 and View2) and a controller for each view (Ctrl1 and Ctrl2). In View1 I'm trying to warn the user before he leaves the page accidentally without saving changes.
I'm using window.onbeforeunload, which works just fine, but my problem is that even though I have onbeforeunload only on one controller, the event seems to be triggered in the second controller as well! But I don't have even have that event in there. When the user leaves a page and there are unsaved changes, he gets warned by the onbeforeunload, if the user dismiss the alert and leaves the page, is he taken back to the second view, if the user tries to leave now this other view, we would also get warned about unsaved changes! When that's not even the case! Why is this happening?
I'm also using $locationChangeStart, which works just fine, but only when the user changes the route, not when they refresh the browser, hit 'back', or try to close it, that's why I'm forced to use onbeforeunload (If you a better approach, please let me know). Any help or a better approach would be greatly appreciated!
//In this controller I check for window.onbeforeunload
app.controller("Ctrl1", function ($scope, ...)){
window.onbeforeunload = function (event) {
//Check if there was any change, if no changes, then simply let the user leave
if(!$scope.form.$dirty){
return;
}
var message = 'If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?';
if (typeof event == 'undefined') {
event = window.event;
}
if (event) {
event.returnValue = message;
}
return message;
}
//This works only when user changes routes, not when user refreshes the browsers, goes to previous page or try to close the browser
$scope.$on('$locationChangeStart', function( event ) {
if (!$scope.form.$dirty) return;
var answer = confirm('If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?')
if (!answer) {
event.preventDefault();
}
});
}
//This other controller has no window.onbeforeunload, yet the event is triggered here too!
app.controller("Ctrl2", function ($scope, ...)){
//Other cool stuff ...
}
I tried checking the current path with $location.path() to only warn the user when he is in the view I want the onbeforeunload to be triggered, but this seems kinda 'hacky' the solution would be to not execute onbeforeunload on the other controller at all.
I added $location.path() != '/view1' in the first which seems to work, but it doesn't seem right to me, besides I don't only have two views, I have several views and this would get tricky with more controllers involved:
app.controller("Ctrl1", function ($scope, ...)){
window.onbeforeunload = function (event) {
//Check if there was any change, if no changes, then simply let the user leave
if($location.path() != '/view1' || !$scope.form.$dirty){
return;
}
var message = 'If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?';
if (typeof event == 'undefined') {
event = window.event;
}
if (event) {
event.returnValue = message;
}
return message;
}
//This works only when user changes routes, not when user refreshes the browsers, goes to previous page or try to close the browser
$scope.$on('$locationChangeStart', function( event ) {
if (!$scope.form.$dirty) return;
var answer = confirm('If you leave this page you are going to lose all unsaved changes, are you sure you want to leave?')
if (!answer) {
event.preventDefault();
}
});
}
Unregister the onbeforeunload event when the controller which defined it goes out of scope:
$scope.$on('$destroy', function() {
delete window.onbeforeunload;
});
I just attempted the above solution, and that wasn't working for me. Even manually typing delete window.onbeforeunload in the console wouldn't remove the function. I needed to set the property to undefined instead.
$scope.$on('$destroy', function(e){
$window.onbeforeunload = undefined;
});
For those of you using angular-ui-router you would use is $stateChangeStart instead of $locationChangeStart, e.g.
$scope.$on('$stateChangeStart', function (event){
if (forbit){
event.preventDefault()
}
else{
return
}
})
As an update to this, for anyone used angular-ui-router I'd now pass $transitions into your controller and use;
$transitions.onStart({}, function ($transition)
{
$transition.abort();
//return false;
});

Resources