Infinite Loop on ui-router's $stateChangeStart - angularjs

Ramping up on angular, and ui-router
And struggling with redirecting to a different state if a precondition is not met:
I tried using an interceptor: (How do I preform a redirect in an angular interceptor).
But someone mentioned that handling $stateChangeState would be more appropriate. But I am still running into an infinite loop:
/**
* Check here for preconditions of state transitions
*/
$rootScope.$on('$stateChangeStart', function(event, toState) {
// which states in accounts to be selected
var accountRequiredStates = ['user.list', 'user.new'];
if(_.contains(accountRequiredStates, toState.name)){
event.preventDefault();
ApiAccount.customGET('get_current').then(function(resp){
// if I have a selected account, go about your business
if(resp.hasOwnProperty('id')){
$state.go(toState.name);
} else { // prompt user to select account
$state.go('user.select_account');
}
})
}
});
Can anyone suggest a better pattern (one that works)
Thanks!
Note: Similar problem different approach here: How do I preform a redirect in an angular interceptor

I don't think there's anything wrong with the general way you're trying to do this, though I'm not an expert. I do see a flaw in the implementation which looks like it could cause an infinite loop. Let's say the user tries to go to 'user.new' state. Your $stateChangeStart listener intercepts that, cancels it, does your customGET; then if the inner if condition is true (resp.hasOwnProperty('id')), you try to send the user to the same 'user.new' state. At which point, your $stateChangeStart listener intercepts it, cancels it, etc., over and over.
The way I avoid this problem in my code is to have a variable (in the service where I declare the listener) to help me bypass that check:
var middleOfRedirecting = false; Inside your inner if block within the resp.hasOwnProperty('id') check, set middleOfRedirecting to true; add a condition at the start of your $stateChangeStart listener to only call event.preventDefault() and redirect if middleOfRedirecting is false. You also would need a $stateChangeSuccess listener to set middleOfRedirecting back to false, resetting it for the next state change. (I feel like there should be a better way than this, but it at least works.)

Related

Why is $locationChangeStart fired upon page load?

Recently I've stumbled upon a very strange code in production that is seemingly using the fact that under some conditions Angular may fire the $locationChangeStart event upon the initial page load. Moreover the next parameter value will be equal to the current value. That seems very odd to me.
I didn't find any relevant documentation for that but here is the fiddle that shows such a situation http://jsfiddle.net/tJSPt/327/
Probably the only difference is that in production we are using the manual Angular bootstrap.
Can anyone explain or point to the trustful sources of information on why is that event triggered upon the page load? Is that something we have to expect or that is just the particularity of the current Angular implementation or our way of using it?
I have experienced this recently but the reason it happened was because I'm using ui-router and the controllerAs syntax. Perhaps you are too?
I stumbled upon this link that helped me out: History should not be changed until after route resolvers have completed
I listened to the $locationChangeStart broadcast but it hit the breakpoint when I entered the state instead of when exciting.
I fixed mine by doing the following:
I listened to $stateChangeStart instead.
I had to move the code above var vm = this;
Here's my code look like after:
// ...
$scope.$on('$stateChangeStart', function (event) {
if (vm.myForm!= null && vm.myForm.$dirty) {
if (!confirm("Are you sure you want to leave this page?")) {
event.preventDefault();
}
}
});
var vm = this;
// vm.xxx = xxxx; .etc ...

Binding to {{}} with firebase simple login auth

I'm using email/password for an app. When I register a user, they choose a username and it binds to a $rootScope variable called authUser.
I then use it in a divbar at the top that indicates that they're logged in as so:
<span>welcome {{authUser}}</span>
This works fine and their chosen username binds to it after they register, as I bind their username to the variable in a callback.
However, when I hard reload the page, it doesn't bind anymore. It seems like it takes a moment for the page to recognize that the user is logged in,
And by that time, angular has already run through its $apply cycle and the only thing I see on the navbar is "welcome".
Is there a way to know at what point (after a hard page reload) the logged in user is recognized? If possible, I'd like to chain some sort of callback or .then() function to bind or apply $rootScope.authUser then.
I guess this may be somewhat related to an asynchronous call, where the data must be used with a promise to guarantee success.
Any tips on this would be appreciated. Ill have access to my source code in about an hour if you need specific details but I think this problem may be more conceptual than about actual code implementation.
Thanks a bunch SO!
If you want to get notified when the user logs in you can register an event listener like this:
$rootScope.$on("$firebaseSimpleLogin:login", function(e, user) {
});
If you just want to know whether the login state has been initialized then you can use $getCurrentUser():
var auth = $firebaseSimpleLogin(this.dbRef);
$scope.loginStateDetermined = false;
auth.$getCurrentUser().then(function (user) {
$scope.loginStateDetermined = true;
});
Checkout the docs for more info:
login-related-events
$getCurrentUser()

locationChangeStart and preventing route change

I'm trying to create basic validation whether user can access some route or not.
I had progress in that, but there's one thing that I can't figure out.
I'm using $locationChangeStart to monitor route changes. Scenario is:
1. if user is logged in, then allow him to access all routes, except auth routes (login, register). I'm checking this by calling method isAuthenticated() from my AuthFactory
2. If user is not logged in, then he access only login and register routes. Any other route should be prevented, and user should be redirected to login in that case.
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl){
if(AuthFactory.isAuthenticated()){
if(AuthFactory.isAuthRoute(newUrl)){
event.preventDefault();
$location.path('/');
}
} else {
if(!AuthFactory.isAuthRoute(newUrl)){
event.preventDefault();
$location.path('/login');
}
}
});
Thing that troubles me, is the one with preventDefault(). If app reaches code with preventDefault(), location.path() that comes after that, simply doesn't work.
However, if I remove event.preventDefault(), location.path() works. Problem with this, is that I need that prevent, in case non-logged tries to access some non-auth page.
Basically, I want to be able to prevent or redirect based on requested route. What is the proper way to do that?
Ok, you need to do this:
var authPreventer = $rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl){
if(AuthFactory.isAuthenticated()){
if(AuthFactory.isAuthRoute(newUrl)){
event.preventDefault();
authPreventer(); //Stop listening for location changes
$location.path('/');
}
}
else {
if(!AuthFactory.isAuthRoute(newUrl)){
event.preventDefault();
authPreventer(); //Stop listening for location changes
$location.path('/login');
}
}
});
You can try using auth in resolves in-order to prevent access to certain routes.
here is the doc,it's not very clear, but you can find plenty of examples out there.
I recently had the same problem and I was finally able to solve it by listening to $routeChangeStart instead of $locationChangeStart (without needing to call $route.reload()).
The documentation for both events is kinda vague... I suppose the $ruteChangeStart event is called before the $locationChangeStart (I'm going to read the source code to fully understand what's happening here).
Ok, I managed to do this using $routeChangeStart. The catch is in using $route.reload(). So above code, should look something like this:
$rootScope.$on('$routeChangeStart', function(event, next, current){
if(AuthFactory.isAuthenticated()){
if(AuthFactory.isAuthRoute(next.originalPath)){
$route.reload();
$location.path('/');
}
} else {
if(!AuthFactory.isAuthRoute(next.originalPath)){
$route.reload();
$location.path('/login');
}
}
});
I put this in my .run method, so all the request are handled here, and I don't need to think about every new route that I (or someone else adds). That's why this looks more clean to me.
If someone has different approach, please share.
Note: just in case, I do my check on backend part also :)

AngularJS (Restangular): Making a promise block? Need to use it for validating a token

I have stumbled upon Restangular for making calls to a rest service. It works great and returns a promise. I need to be able to have the call block. The reason for this is on a fresh page reload I am technically not loggged in but I may have a token stored in a cookie. i would like to validate this token against a rest service. Problem is that I need it to block.
If a timeout occurs or if its not valid that i can treat teh user as not authenticated.
This is the reason for wanting to block is that i would like to redirect them using $location.path to a new URL it not a valid token.
This doesn't happen on a specific route so i can't use resolve which is blocking. It technically happens on every route - I use the $on.$routeChangeStart and check an internal variable got LoggedIn or not, if not logged in i check for the stored token.
This happens on each Page refresh but not while navigating inside the application.
The affect I am trying to get is how Gmail works.
Look forward to any insight anyone has on this
Thanks
Basically you need to ensure that some asynchronous action occurs prior to any route change occurring, and in this case the action is authenticating a user.
What you can do is use the $routeChangeStart event that's emitted in order to add a property to the resolve object on the route like so:
function authenticate() {
if ( user.isAuthenticated ) {
return;
}
// Just fake it, but in a real app this might be an ajax call or something
return $timeout(function() {
user.isAuthenticated = true;
}, 3000);
}
$rootScope.$on( "$routeChangeStart", function( e, next ) {
console.log( "$routeChangeStart" );
next.resolve = angular.extend( next.resolve || {}, {
__authenticating__: authenticate
});
});
Since angular will wait for any promises in the resolve object to be fulfilled before proceeding, you can just use a pseudo dependency as in the example. Using something like that, you should be able to guarantee that your user is authenticating prior to any routes successfully executing.
Example: http://jsfiddle.net/hLddM/
I think the best way to do this might be to push the user around with $location.path, You can use .then() to effectively force a wait by leaving the user on a loading page.
var currentPath = $location.path();
$location.path(loadingScreen);
//Assuming you have some sort of login function for ease.
Restangular.login(token).then(
function(result) {
$location.path(currentPath)
},
function(error) {
$location.path(logInScreen)
}
);
If you're using ui-router, you could move to another state with the same URL, where you'd use that Restangular.login with the then, and in case of success go back to the "logged in" state, otherwise, go to the "log in" state where the user must enter his username and password.
If you're not using ui-router, you could implement something like that with some ng-switch.
So, upon arrival to the screen, you do that Restangular.login and by default you show loading page by setting some boolean to true. Then, if it doesn't succedd, you send him to the login, otherwise, you set loading to false and show page.
Anyway, I'd strongly recommend using ui-router, it rocks :)
Hope this works!

AngularJS - Detecting, stalling, and cancelling route changes

I can see the event $routeChangeStart in my controller, but I don't see how to tell Angular to stay. I need to popup something like "Do you want to SAVE, DELETE, or CANCEL?" and stay on the current "page" if the user selects cancel. I don't see any events that allow listeners to cancel a route change.
You are listening to the wrong event, I did a bit of googling but couldn't find anything in the docs. A quick test of this:
$scope.$on("$locationChangeStart", function(event){
event.preventDefault();
})
In a global controller prevented the location from changing.
The documented way of doing this is to use the resolve property of the routes.
The '$route' service documentation says that a '$routeChangeError' event is fired if any of the 'resolve' promises are rejected.1 That means you can use the '$routeProvider' to specify a function which returns a promise that later gets rejected if you would like to prevent the route from changing.
One advantage of this method is that the promise can be resolved or rejected based on the results of asynchronous tasks.
$scope.$watch("$locationChangeStart", function(event){
event.preventDefault();
});
You can do like this as well. Benefit of doing this way is that it
doesn't trigger through if() statement with $on ...as you well see
that below code will trigger no matter what the condition is:
if(condition){ //it doesn't matter what the condition is true or false
$scope.$on("$locationChangeStart", function(event){ //this gets triggered
event.preventDefault();
});
}

Resources