Why stateChangeStart event called twice? - angularjs

I use $stateChangeStart for check user when change state.
This is my code
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams, options) {
if (!toState.data.require_authentication) {
return;
}
// Because state change is called two times, to prevent infinite loop I remove is_authenticate for next re-check and return
if (toState.data.is_authenticate) {
toState.data.is_authenticate = false;
return;
}
event.preventDefault();
toState.data.is_authenticate = false;
// Check validity of token
authService.checkAuthToken(function(result, status) {
if (result.fn.result.done) {
toState.data.is_authenticate = true;
//$state.go(toState, toParams, options);
$state.go(toState, toParams);
} else {
$log.debug("AuthToken expire/wrong");
storageService.reset();
$state.go("page.login");
}
return;
});
});
With some console log I see that when $state.go is fired, for some reason $stateChangeStart is fired again, so I need to return out by a boolean params or I enter in an infinite loop...
Why?

I suggest to check some articles on UI Router and Authentication, however you can substitute $state.go() with $state.transitionTo() and setting notify option to false to avoid broadcast of $stateChangeStart event.
Interesting articles:
http://lean.mean.software/2015/01/05/authentication-with-the-angularjs-ui-router/
http://solidfoundationwebdev.com/blog/posts/require-authentication-for-certain-routes-with-ui-router-in-angularjs

Related

How to perform an action when the state goes to the just previous state in angular

I am new to angular.I want to perform a certain action when the user clicks on the back button of the browser and the state goes back to the previous state.
I used below code to do that ,but that doesn't get called when back button of browser is clicked.
$scope.$on('$destroy', function() {
console.log("scope.on called ");
console.log($scope.viewTest.context.showMore)
$scope.viewTest.context.showMore = true;
console.log($scope.viewTest.context.showMore);
});
For ui-routing, you can use the code below
$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams)
{
if (toState.name === $rootScope.previousState )
{
// u can any 1 or all of below 3 statements
event.preventDefault(); // to Disable the event
//Do whatever you want
}
});

$scope.$on not listening after browser refresh

I am using angular ui-router.
I have the following structure :
state a controller parentA
state a.b Controller childB
I have a resolved on state a.b that broadcast event 'event1' and I have a listener on parentA controller.
It works fine when I login. But on subsequent browser refreshes, I can see the event being broadcast but the listener is not working.
Any help is appreciated!
Might this code helps you
$scope.$on('$viewContentLoaded', onLoaded);
$scope.$on('viewContentLoadComplete', onLoadComplete);
$rootScope.$on('$stateChangeStart',
function (event, toState, toParams, fromState, fromParams) {
$scope.isViewLoading = true;
});
$rootScope.$on('$stateChangeSuccess',
function(event, toState, toParams, fromState, fromParams) {
$scope.isViewLoading = false;
});
function onLoaded() {
$scope.isViewLoading = true;
$scope.$broadcast('viewContentLoadComplete');
}
function onLoadComplete() {
$scope.isViewLoading = false;
}
When you traverse between the views be it parent or child you always gonna fall is this condition where you can handle the stuffs you wish.
Better code than this is also welcome with brief explanation.
You can define this in your main controller.Here $scope.isViewLoading is just a gif image which is set to hide and show as loader.

Ui-Router $state.go inside $on('$stateChangeStart') is cauzing an infinite loop

I'm trying to introduce login into the way the user navigates accross the application.
I pretend to redirect the user to the page were he was before he navigate to the login page if that page meets specific requirements
Preventing the event from the $stateChangeStart stop's the state change like expected but when i run the $state.go('into_somewhere') i enter an infinit loop
My angular version is 1.3.1 and the ui-router is the latest
.factory('RouteHistory', function ($rootScope,$log, $state, Auth, $urlRouter, $timeout) {
// after the user enter a page
var currentState = '';
// when the user is trying to access a page that he has not permissions
// or that requires the user to be logged in
var pendingState = '';
var isMenuTogglerVisible = false;
var skipFromStateVal = true;
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
event.preventDefault();
if (toState.name == 'login' && fromState.name != 'login'){
$log.log('Ui-router: changing to login');
// $urlRouter.sync();
$state.go('login')
//pendingState = fromState;
//$log.log('Peding state updated to:' + pendingState.name );
//$urlRouter.sync();
}
if (fromState.name == 'login' && Auth.isLoggedIn()) {
$log.log('Ui-router: going from login');
//$state.go(fromState.name);
$timeout(function(){
// $state.go('home', null, {/*reload: true, location: 'replace'*/});
$state.go('browse-machine');
//$urlRouter.sync();
},2000)
}
$log.log({
'toState': toState,
'toParams': toParams,
'fromState': fromState,
'fromParams': fromParams
})
})
return {
};
});
In general I would say, let's redirect ($state.go()) only if needed. In other cases, get out from the event listener:
if (toState.name === 'login' ){
// doe she/he try to go to login? - let him/her go
return;
}
if(Auth.isLoggedIn()){
// is logged in? - can go anyhwere
return;
}
// else
$state.go('login')
This is simplified logic, but shows, that we should change to execution only if needed. There are some other examles with more detailed implementation and plunkers:
Confusing $locationChangeSuccess and $stateChangeStart
Angular UI Router: nested states for home to differentiate logged in and logged out
other example of log in
angular ui-router login authentication
As provided in the comment, there was plunker, which I changed like this here
...
// three new lines
if (toState.name === 'specialRoute'){
return;
}
if (fromState.name=='route1'){
event.preventDefault();
$state.go('specialRoute')
}
And this is not looping anymore. Please, check it here
You should use the notify option :
$state.go('your.state',{ your params },{notify: false});
This will prevent stateChangeStart to fire again.
This answer helped me:
$urlRouterProvider.otherwise( function($injector, $location) {
var $state = $injector.get("$state");
$state.go("app.home");
});
Original:
Why does AngularJS with ui-router keep firing the $stateChangeStart event?
I simply used $location.path('every/where') instead of $state.go('every/where')
:) .
The infinite loop is partly caused by
if (toState.name == 'login' ...) { $state.go('login'); ...
..which says if you're going to the login state, then go to the login state.
...And calling event.preventDefault() as the first line in the event handler doesn't help. When you use go() to go to the login screen (or anywhere else), that state change is also prevented by event.preventDefault(). It should only be used within an if.
Your entire $stateChangeStart handler should instead be...
if (!Auth.isLoggedIn() && toState.name != 'login') {
event.preventDefault();
Auth.desiredState = toState.name;
$state.go('login');
}
...which reads naturally. "If you're not logged in and you're not already going to the login screen, then stop what you're doing, I'll remember where you wanted to go, and you now go to the login screen."
Later your Auth object will issue a $state.go(Auth.desiredState) when it's satisfied with the user.
It works for me, Below code helps to get rid of infinite loop
let firstPass = true;
$scope.$on('$stateChangeStart', function(event, toState, toParams) {
if ($scope.addForm.$dirty && firstPass) {
event.preventDefault();
ConfirmationDialog.openNavigateAwayConfirmationModal().then(function () {
firstPass = false;
return $state.go(toState, toParams);
});
firstPass = true;
}
});

How to prevent user from navigating away when form is dirty

I have an angularJs app that requires authentication for most pages. I've implemented the checks similar to http://www.frederiknakstad.com/2014/02/09/ui-router-in-angular-client-side-auth/
In angular.module('myModule').run(), I have:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams){
if(toState.authenticate){
event.preventDefault();
var toUrl = $state.href(toState, toParams);
var promise = authService.isAuthenticated();
if (promise){
promise.then(function(){
// successful authentication proceed to requested state
$state.go(toState.name, toParams, {notify: false, inherit: false}).then(function(state){
$rootScope.$broadcast('$stateChangeSuccess', state.self, toParams);
});
},function(){
// failed authentication proceed to login
$state.go('login', {
next: toUrl
});
});
} else {
$state.go('login', {
next: toUrl
});
}
}
});
This is working just fine, however, when I tried to prevent a state change from within a controller when a form is dirty, it doesn't quite work. Here is how I add the listener in my controller:
$scope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
if($scope.forms.myForm.$dirty) {
event.preventDefault();
modalAlert({
text: 'You have unsaved changes, are you sure you wish to continue'
}).result.then(function() {
// User is OK leaving dirty form, clear dirty flag to prevent this pop-up showing again and re-route
$scope.forms.myForm.$setPristine();
$state.go(toState, toParams);
}, function() {
// Cancelled request, stay on dirty form, don't need to do anything
});
}
});
Basically, what is happening, is the first $stateChangeStart is called, the auth is resolved, and it does a $state.go() to the next state we were going to. Meanwhile, the modal pops up asking to confirm and if I hit cancel, it reverts back to the previous state, but all my changes are gone because the auth checking already changed the state and lost all my changes. If the page I'm going to does not require authentication, then it works as expected.
How can I make my controller's $stateChangeStart execute first?

stop angular-ui-router navigation until promise is resolved

I want to prevent some flickering that happens when rails devise timeout occurs, but angular doesn't know until the next authorization error from a resource.
What happens is that the template is rendered, some ajax calls for resources happen and then we are redirected to rails devise to login. I would rather do a ping to rails on every state change and if rails session has expired then I will immediately redirect BEFORE the template is rendered.
ui-router has resolve that can be put on every route but that doesn't seem DRY at all.
What I have is this. But the promise is not resolved until the state is already transitioned.
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
//check that user is logged in
$http.get('/api/ping').success(function(data){
if (data.signed_in) {
$scope.signedIn = true;
} else {
window.location.href = '/rails/devise/login_path'
}
})
});
How can I interrupt the state transition, before the new template is rendered, based on the result of a promise?
I know this is extremely late to the game, but I wanted to throw my opinion out there and discuss what I believe is an excellent way to "pause" a state change. Per the documentation of angular-ui-router, any member of the "resolve" object of the state that is a promise must be resolved before the state is finished loading. So my functional (albeit not yet cleaned and perfected) solution, is to add a promise to the resolve object of the "toState" on "$stateChangeStart":
for example:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
toState.resolve.promise = [
'$q',
function($q) {
var defer = $q.defer();
$http.makeSomeAPICallOrWhatever().then(function (resp) {
if(resp = thisOrThat) {
doSomeThingsHere();
defer.resolve();
} else {
doOtherThingsHere();
defer.resolve();
}
});
return defer.promise;
}
]
});
This will ensure that the state-change holds for the promise to be resolved which is done when the API call finishes and all the decisions based on the return from the API are made. I've used this to check login statuses on the server-side before allowing a new page to be navigated to. When the API call resolves I either use "event.preventDefault()" to stop the original navigation and then route to the login page (surrounding the whole block of code with an if state.name != "login") or allow the user to continue by simply resolving the deferred promise instead of trying to using bypass booleans and preventDefault().
Although I'm sure the original poster has long since figured out their issue, I really hope this helps someone else out there.
EDIT
I figured I didn't want to mislead people. Here's what the code should look like if you are not sure if your states have resolve objects:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if (!toState.resolve) { toState.resolve = {} };
toState.resolve.pauseStateChange = [
'$q',
function($q) {
var defer = $q.defer();
$http.makeSomeAPICallOrWhatever().then(function (resp) {
if(resp = thisOrThat) {
doSomeThingsHere();
defer.resolve();
} else {
doOtherThingsHere();
defer.resolve();
}
});
return defer.promise;
}
]
});
EDIT 2
in order to get this working for states that don't have a resolve definition you need to add this in the app.config:
var $delegate = $stateProvider.state;
$stateProvider.state = function(name, definition) {
if (!definition.resolve) {
definition.resolve = {};
}
return $delegate.apply(this, arguments);
};
doing if (!toState.resolve) { toState.resolve = {} }; in stateChangeStart doesn't seem to work, i think ui-router doesn't accept a resolve dict after it has been initialised.
I believe you are looking for event.preventDefault()
Note: Use event.preventDefault() to prevent the transition from happening.
$scope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){
event.preventDefault();
// transitionTo() promise will be rejected with
// a 'transition prevented' error
})
Although I would probably use resolve in state config as #charlietfl suggested
EDIT:
so I had a chance to use preventDefault() in state change event, and here is what I did:
.run(function($rootScope,$state,$timeout) {
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){
// check if user is set
if(!$rootScope.u_id && toState.name !== 'signin'){
event.preventDefault();
// if not delayed you will get race conditions as $apply is in progress
$timeout(function(){
event.currentScope.$apply(function() {
$state.go("signin")
});
},300)
} else {
// do smth else
}
}
)
}
EDIT
Newer documentation includes an example of how one should user sync() to continue after preventDefault was invoked, but exaple provided there uses $locationChangeSuccess event which for me and commenters does not work, instead use $stateChangeStart as in the example below, taken from docs with an updated event:
angular.module('app', ['ui.router'])
.run(function($rootScope, $urlRouter) {
$rootScope.$on('$stateChangeStart', function(evt) {
// Halt state change from even starting
evt.preventDefault();
// Perform custom logic
var meetsRequirement = ...
// Continue with the update and state transition if logic allows
if (meetsRequirement) $urlRouter.sync();
});
});
Here is my solution to this issue. It works well, and is in the spirit of some of the other answers here. It is just cleaned up a little. I'm setting a custom variable called 'stateChangeBypass' on the root scope to prevent infinite looping. I'm also checking to see if the state is 'login' and if so, that is always allowed.
function ($rootScope, $state, Auth) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
if($rootScope.stateChangeBypass || toState.name === 'login') {
$rootScope.stateChangeBypass = false;
return;
}
event.preventDefault();
Auth.getCurrentUser().then(function(user) {
if (user) {
$rootScope.stateChangeBypass = true;
$state.go(toState, toParams);
} else {
$state.go('login');
}
});
});
}
as $urlRouter.sync() doesn't work with stateChangeStart, here's an alternative:
var bypass;
$rootScope.$on('$stateChangeStart', function(event,toState,toParams) {
if (bypass) return;
event.preventDefault(); // Halt state change from even starting
var meetsRequirement = ... // Perform custom logic
if (meetsRequirement) { // Continue with the update and state transition if logic allows
bypass = true; // bypass next call
$state.go(toState, toParams); // Continue with the initial state change
}
});
To add to the existing answers here, I had the exact same issue; we were using an event handler on the root scope to listen for $stateChangeStart for my permission handling. Unfortunately this had a nasty side effect of occasionally causing infinite digests (no idea why, the code was not written by me).
The solution I came up with, which is rather lacking, is to always prevent the transition with event.preventDefault(), then determine whether or not the user is logged in via an asynchronous call. After verifying this, then use $state.go to transition to a new state. The important bit, though, is that you set the notify property on the options in $state.go to false. This will prevent the state transitions from triggering another $stateChangeStart.
event.preventDefault();
return authSvc.hasPermissionAsync(toState.data.permission)
.then(function () {
// notify: false prevents the event from being rebroadcast, this will prevent us
// from having an infinite loop
$state.go(toState, toParams, { notify: false });
})
.catch(function () {
$state.go('login', {}, { notify: false });
});
This is not very desirable though, but it's necessary for me due to the way that the permissions in this system are loaded; had I used a synchronous hasPermission, the permissions might not have been loaded at the time of the request to the page. :( Maybe we could ask ui-router for a continueTransition method on the event?
authSvc.hasPermissionAsync(toState.data.permission).then(continueTransition).catch(function() {
cancelTransition();
return $state.go('login', {}, { notify: false });
});
The on method returns a deregistration function for this listener.
So here is what you can do:
var unbindStateChangeEvent = $scope.$on('$stateChangeStart',
function(event, toState, toParams) {
event.preventDefault();
waitForSomething(function (everythingIsFine) {
if(everythingIsFine) {
unbindStateChangeEvent();
$state.go(toState, toParams);
}
});
});
I really like the suggested solution by TheRyBerg, since you can do all in one place and without too much weird tricks. I have found that there is a way to improve it even further, so that you don't need the stateChangeBypass in the rootscope. The main idea is that you want to have something initialized in your code before your application can "run". Then if you just remember if it's initialized or not you can do it this way:
rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState) {
if (dataService.isInitialized()) {
proceedAsUsual(); // Do the required checks and redirects here based on the data that you can expect ready from the dataService
}
else {
event.preventDefault();
dataService.intialize().success(function () {
$state.go(toState, toParams);
});
}
});
Then you can just remember that your data is already initialized in the service the way you like, e.g.:
function dataService() {
var initialized = false;
return {
initialize: initialize,
isInitialized: isInitialized
}
function intialize() {
return $http.get(...)
.success(function(response) {
initialized=true;
});
}
function isInitialized() {
return initialized;
}
};
You can grab the transition parameters from $stateChangeStart and stash them in a service, then reinitiate the transition after you've dealt with the login. You could also look at https://github.com/witoldsz/angular-http-auth if your security comes from the server as http 401 errors.
I ran in to the same issue Solved it by using this.
angular.module('app', ['ui.router']).run(function($rootScope, $state) {
yourpromise.then(function(resolvedVal){
$rootScope.$on('$stateChangeStart', function(event){
if(!resolvedVal.allow){
event.preventDefault();
$state.go('unauthState');
}
})
}).catch(function(){
$rootScope.$on('$stateChangeStart', function(event){
event.preventDefault();
$state.go('unauthState');
//DO Something ELSE
})
});
var lastTransition = null;
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams, options) {
// state change listener will keep getting fired while waiting for promise so if detect another call to same transition then just return immediately
if(lastTransition === toState.name) {
return;
}
lastTransition = toState.name;
// Don't do transition until after promise resolved
event.preventDefault();
return executeFunctionThatReturnsPromise(fromParams, toParams).then(function(result) {
$state.go(toState,toParams,options);
});
});
I had some issues using a boolean guard for avoiding infinite loop during stateChangeStart so took this approach of just checking if the same transition was attempted again and returning immediately if so since for that case the promise has still not resolved.

Resources