Spinner loading on routechangestart with Authentication library - javascript

I'm using the Hot Towel angular library (https://github.com/johnpapa/hottowel-angular-bower) on a project I inherited from a senior developer.
I'm also incorporating Auth0's authentication library for Angular.
I need to restrict some routes to authenticated users. To do that, I set some route properties.
isLogin: true for routes which is restricted to non-authenticated users.
requiresLogin: true for routes needing authentication, and the opposite for those who don't. In order to check these properties on each runthrough, I use $rootScope.$on('$routeChangeStart' function()).
app.run(function ($rootScope, $location, auth, common, config) {
var getLogFn = common.logger.getLogFn,
log = getLogFn('auth handle'),
events = config.events;
$rootScope.$on('$routeChangeSuccess', function (e, nextRoute, currentRoute) {
if (nextRoute.$$route && nextRoute.$$route.settings && nextRoute.$$route.settings.requiresLogin) {
if (!auth.isAuthenticated) {
$location.path('/login');
log('User not authenticated');
}
}
if (nextRoute.$$route && nextRoute.$$route.settings && nextRoute.$$route.settings.isLogin) {
if (auth.isAuthenticated) {
$location.path('/');
log('User is authenticated');
}
}
})
});
Now, it seems this is interfering with the spinner functionality included with Hot-Towel. In Shell.js I find the following:
$rootScope.$on('$routeChangeStart',
function (event, next, current) { toggleSpinner(true); }
);
$rootScope.$on(events.controllerActivateSuccess,
function (data) { toggleSpinner(false); }
);
$rootScope.$on(events.spinnerToggle,
function (data) { toggleSpinner(data.show); }
);
What happens is that the spinner never stops spinning (e.g. vm.isBusy = true because a controller is never activated and resetting this), how would I work around this?

One idea is that you could use an event ($broadcast) to notify when the un authorized access is made so it is then received by the controller that turns off the spinner.

I haven't studied your code for very long but shouldn't you use $routeChangeSuccess to stop the spinner?
$scope.isViewLoading = false;
$scope.$on('$routeChangeStart', function() {
$scope.isViewLoading = true;
});
$scope.$on('$routeChangeSuccess', function() {
$scope.isViewLoading = false;
});

Related

Angular state change after a promise

I'm not sure if this is a duplicate or not, but I didn't manage to find anything that worked for me, so I'm posting this question.
I have a situation where I need to get values from database before directing user to certain routes, so I could decide what content to show.
If I move e.preventDefault() right before $state.go(..) then it works, but not properly. Problem is that it starts to load default state and when it gets a response from http, only then it redirects to main.home. So let's say, if the db request takes like 2 seconds, then it takes 2 seconds before it redirects to main.home, which means that user sees the content it is not supposed to for approximately 2 seconds.
Is there a way to prevent default at the beginning of state change and redirect user at the end of state change?
Also, if we could prevent default at the beginning of state change, then how could we continue to default state?
(function(){
"use strict";
angular.module('app.routes').run(['$rootScope', '$state', '$http', function($rootScope, $state, $http){
/* State change start */
$rootScope.$on('$stateChangeStart', function(e, to, toParams, from, fromParams){
e.preventDefault();
$http
.get('/url')
.error(function(err){
console.log(err);
})
.then(function(response){
if( response.data === 2 ){
// e.preventDefault()
$state.go('main.home');
}
// direct to default state
})
}
}]);
});
You could add a resolve section to your $stateProviderConfig.
Inside the resolve you can make a request to the databse and check required conditions. If case you don't want user to acces this page you can use $state.go() to redirect him elsewhere.
Sample config:
.state({
name: 'main.home',
template: 'index.html',
resolve: {
accessGranted: ['$http', '$state', '$q',
function($http, $state, $q) {
let deffered = $q.defer();
$http({
method: 'GET',
url: '/url'
}).then(function(data) {
if (data === 2) {
// ok to pass the user
deffered.resolve(true);
} else {
//no access, redirect
$state.go('main.unauthorized');
}
}, function(data) {
console.log(data);
//connection error, redirect
$state.go('main.unauthorized');
});
return deffered.promise;
}
]
}
});
Documentation of the resolve is available here
Note that you could use Promise object instead of $q service in case you don't need to support IE
One way to handle this situation is adding an interceptor as follows.
.config(function ($httpProvider) {
$httpProvider.interceptors.push('stateChangeInterceptor');
}).factory('stateChangeInterceptor', function ($q, $window,$rootScope) {
return {
'response': function(response) {
var isValid = true;//Write your logic here to validate the user/action.
/*
* Here you need to allow all the template urls and ajax urls which doesn't
*/
if(isValid){
return response;
}
else{
$rootScope.$broadcast("notValid",{statusCode : 'INVALID'});
}
},
'responseError': function(rejection) {
return $q.reject(rejection);
}
}
})
Then handle the message 'notValid' as follows
.run(function($state,$rootScope){
$rootScope.$on("notValid",function(event,message){
$state.transitionTo('whereever');
});
})

Automatically $broadcast data

I'm trying to$broadcast some data from one controller to another using the$rootScope .
It appears to work fine if I use a trigger like an ng-click to run the function that will broadcast but how do it without that?
As you can see in the fiddle, I have the broadcast in a $scope.cast function so why is it not working if I run the function like this: $scope.cast(); ?
Fiddle: https://jsfiddle.net/kjgj7Ldz/19/
I need this because I am getting some data in the first controller and when that finishes, I want to automatically broadcast it without ng-click, ng-change or any other triggers.
Is $broaadcast a wrong thing to do in this scenario? If so, how can I achieve data communication between those two controllers?
You can avoid using scope for commutication between controllers by creating a simple pub-sub service that handles the communtication channel for you. For example it can deliver all messages for late subscribers. Demo.
app.service('MQ', function() {
var listeners = [],
messages = [];
return {
pub: function(message) {
listeners.slice(0).forEach(function(listener) {
try {
listener(message)
} catch (ignored) {
console.log(ignored)
}
})
// save message for late subscribers.
messages.push(message)
},
sub: function(listener) {
// deliver all messages
messages.slice(0).forEach(function(message) {
try {
listener(message)
} catch (ignored) {
console.log(ignored)
}
})
// save listener
listeners.push(listener)
// create unbinder
return function() {
listeners.splice(listeners.indexOf(listener), 1)
}
}
}
})
app.controller('Controller1', ['$scope', 'MQ', function($scope, MQ) {
MQ.pub('John Snow')
$scope.cast = function() {
MQ.pub(Math.random())
}
}]);
app.controller('Controller2', ['$scope', 'MQ', function($scope, MQ) {
var unsub = MQ.sub(function(message) {
$scope.message = message
})
// clean-up bindings on scope destroy.
$scope.$on('$destroy', unsub)
}]);

angular, why is page reloaded when changing state application

I have a angular webapp that is using a pre-produced theme/framework called fuse (http://withinpixels.com/themes/fuse), this theme already has an app structure and code written to make the creation of apps easier.
We added some pages (which include sidemenu items), the problem is, when you tap one of the links in the sidebar, the whole page seems to be reloaded or at least a animate-slide-up is played 2 times on the index main div, I traced down one part of the problem to the configuration module of the page:
$stateProvider.state('app.pages_dashboard', {
url : '/dashboard',
views : {
'main#' : {
templateUrl: 'app/core/layouts/vertical-navigation.html',
controller : 'MainController as vm'
},
'content#app.pages_dashboard': {
templateUrl: 'app/main/dashboard/dashboard.html',
controller : 'DashboardController as vm'
},
'navigation#app.pages_dashboard': {
templateUrl: 'app/navigation/layouts/vertical-navigation/navigation.html',
controller : 'NavigationController as vm'
},
'toolbar#app.pages_dashboard': {
templateUrl: 'app/toolbar/layouts/vertical-navigation/toolbar.html',
controller : 'ToolbarController as vm'
},
},
bodyClass: 'login',
needAuth: true,
onStateChangeStart: function(event, state, auth, api) {
console.log('onStateChangeStart on DASHBOARD');
api.getUserCard.save({}, {}, function (response){
if (!response.result) {
state.go('app.pages_claimcard');
}
});
}
});
and the configuration module of the app
angular
.module('fuse')
.run(runBlock);
/** #ngInject */
function runBlock($rootScope, $timeout, $state, $auth, api)
{
console.log('INDEX.RUN loaded');
// Activate loading indicator
var stateChangeStartEvent = $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams)
{
// console.log('started change event');
// console.log(toState);
// console.log(fromState);
// check if authentication needed
if (toState.needAuth) {
// redirect to login page
if (!$auth.isAuthenticated()) {
event.preventDefault();
$state.go('app.pages_auth_login');
}
}
if (toState.onStateChangeStart) {
// THIS CAUSES ONE OF THE RELOADS
// toState.onStateChangeStart(event, $state, $auth, api);
}
$rootScope.loadingProgress = true;
});
// De-activate loading indicator
var stateChangeSuccessEvent = $rootScope.$on('$stateChangeSuccess', function ()
{
$timeout(function ()
{
$rootScope.loadingProgress = false;
});
});
// Store state in the root scope for easy access
$rootScope.state = $state;
// Cleanup
$rootScope.$on('$destroy', function ()
{
stateChangeStartEvent();
stateChangeSuccessEvent();
});
}
as you can see I commented the the toState OnStateChangeStart function, and that got rid of one the 'reloads' of the application, so basically have 2 questions:
Why does the onStateChangeStart function on the toState state causes the page to reload?
I have no idea what might be causing the other page reload, any ideas?
I found the problem, my states were defined as:
'app.pages.dashboard'
however there was never a declaration for
'app.pages'
so UI-router was trying its best to sort this mess, anyways, always remember to properly declare your states and everything should be fine.

event.preventDefault() not waiting till promise is resolved

I'm using event.preventDefault() technique to conditionally redirect user to an appropriate state based on a flag value which I'm fetching from backend. Eveything seems to be working fine except that sometimes the actual page is rendered and then redirection happens and that too in very few cases. This render of actual page is also for a very short duration. Following is the code snippet which I'm using:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
if (fromState.name === '' && toState.name === "myState"){
Auth.isLoggedInAsync(function(loggedIn) {
if (!loggedIn) {
event.preventDefault();
$state.go('state1');
} else {
event.preventDefault();
Auth.getNextStateForUser()
.then(function(data) {
console.log(data.nextState);
$state.go(data.nextState);
})
.catch(function(err){
console.log(err);
$state.go(toState.name);
});
}
});
The logic which I'm following is, check if a user is logged in, then redirect to state1, else redirect to next appropriate state based on nextstateValue fetched from backend. If some error occurs, open the actual page without any redirection.
I hope I make my myself clear with what I'm doing. Just want to know if the issue which I'm facing is a genuine issue or there is something wrong which I'm doing from my side.
I have posted it on github page as well but it seems that I'm the only one who is facing this issue.
Do check this for more info:
https://github.com/angular-ui/ui-router/issues/2088
Thanks
Edit: Based on the answers I have updated my code for using ui-router resolve and converted my isLoggedInAsyc to return promise.
resolve: {
promiseState: function(Auth, $state, $q, $stateParams) {
var deferred = $q.defer();
Auth.isLoggedInSync()
.then(function(isLoggedIn){
console.log(isLoggedIn);
if(isLoggedIn) {
Auth.getNextStateForUser()
.then(function(data) {
console.log(data.nextState);
$state.go(data.nextState);
deferred.resolve(data);
})
.catch(function(error) {
console.log(error);
deferred.reject(error);
});
}
deferred.resolve(isLoggedIn);
})
.catch(function(err){
console.log(err);
deferred.reject(err);
});
return deferred.promise;
}
}
And my isLoggedIn() returning promise.
isLoggedInSync: function(callback) {
var cb = callback || angular.noop;
var deferred = $q.defer();
if(currentUser.hasOwnProperty('$promise')) {
currentUser.$promise.then(function() {
deferred.resolve(true);
return cb(true);
}).catch(function() {
deferred.resolve(false);
return cb(false);
});
} else if(currentUser.hasOwnProperty('role')) {
deferred.resolve(true);
} else {
deferred.resolve(false);
}
return deferred.promise;
}
As mentioned by #milaenlempera, all Angular events are synchronous and there is no real way for you to nicely handle asynchrony in this instance. We had this issue in our production code and our solution was to use the resolve functionality in ui-router. This is how we solved it (plugged into your code obviously)
.state({
name: 'myState',
resolve: {
principal: ['Auth', '$state', '$q', function(Auth, $state, $q) {
var deferred = $q.defer();
Auth.isLoggedInAsync(function(loggedIn) {
if(!isLoggedIn) {
deferred.resolve($state.go('state1'));
return;
}
return Auth.getNextStateForUser()
.then(function(data) {
$state.go(data.nextState);
})
.catch(function(error) {
deferred.reject(error);
});
});
return deferred.promise;
}]
}
});
I'd recommend turning Auth.isLoggedInAsync to return a promise.
Also, your original code will cause an infinite loop if Auth.getNextStateForUser() is rejected. It will attempt to go to the state with the name toState.name, which is the state that just caused the error, which will cause another error and cause it to go to toState.name....
Because resolve cascades, you can implement child states if you want to share rules for permissions. It does really suck that there is no way to just put a property on the state and handle asynchronous permission checking elsewhere, but that's the hand we have been dealt at the moment.
As per comments, here's an example of promise chaining with your amended code.
promiseState: ['Auth', '$state', function(Auth, $state) {
return Auth.isLoggedInSync()
.then(function(isLoggedIn) {
if(isLoggedIn) {
return Auth.getNextStateForUser().then(function(data) {
return data.nextState;
});
}
// If the user isn't logged in, return the state that you should go to instead
return 'state1';
})
.then(function(nextState) {
return $state.go(nextState);
});
}]
Angular events are synchoronously.
If you need stop it, you must do it synchronously in event handler function.
In you example is asynchronously checked login, meanwhile routing continue. This is why you see page and to redirect happens afterwards.
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){
// here check if route can be display (synchronously)
// or event.preventDefault()
Auth.isLoggedInAsync(function(loggedIn) {
if (loggedIn) {
// continue to original route or go to state by server data
} else {
// redirect to login page
}
});
you can be inspired in my solution here

Proper way to pause routing in angular

I have an angular application with guest functionality. It means what i create a guest account for all unauthorized users in background. And i need to pause routing till guest account will be created and i can specify auth token to all other request. At the moment i'm doing it by adding of resolve param to all routes.
.config(function ($routeProvider) {
var originalWhen = $routeProvider.when;
$routeProvider.when = function (path, route) {
if (path && path.indexOf('sign') === -1) {
route.resolve = route.resolve || {};
route.resolve.userSync = ['User', function (User) {
return User.isSynchronized.promise;
}];
}
return originalWhen.call(this, path, route);
};
});
But it looks like not very nice solution. Can anyone give me advice how do it by the proper way?
You can listen to rootScope locationChangeStart event
.run(['$rootScope', 'User', function ($rootScope, User) {
$rootScope.userLoggedIn = false;
var preventLocationChangeUnregister = $rootScope.$on('$locationChangeStart', function (event, newUrl, oldUrl) {
if ($rootScope.userLoggedIn === false && newUrl.indexOf('sign') === -1) {
event.preventDefault(); // This prevents the navigation from happening
}
});
User.isSynchronized.promise.then(function () {
preventLocationChangeUnregister(); //By calling this your event listener for the $locationChangeStart event will be unsubscribed
//$rootScope.userLoggedIn = true; //Or you can set userLoggedIn to true without unregistering event
});
}])

Categories

Resources