How to handle stateChangeStart event at controller level in angularjs? - angularjs

I am trying to redirect to home page when specific views are refreshed in browser. If I handle stateChangeStart event on rootScope level, it all works fine and navigates to home page.
var payApp = angular.module('pay', ['ui.router', 'pay.controllers', 'pay.services', 'exceptionOverride']);
payApp.run(['$templateCache', '$rootScope', '$state', '$stateParams', '$window', '$location', function ($templateCache, $rootScope, $state, $stateParams, $window, $location) {
$templateCache.put(baseUrl, angular.element('#ui-view').html());
// Allows to retrieve UI Router state information from inside templates
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
$rootScope.$on('$stateChangeSuccess', function (event, toState) {
// Sets the layout name, which can be used to display different layouts (header, footer etc.)
// based on which page the user is located
$rootScope.layout = toState.layout;
});
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams, options) {
//When payment or order page is refreshed, navigate to home
if (fromState.name == '' && (toState.name == 'payment' || toState.name == 'order')) {
$location.url('/');
}
});
var windowElement = angular.element($window);
windowElement.on('beforeunload', function (event) {
var path = $location.path();
if (path == '/payment' || path == '/order') {
event.preventDefault();
}
});
}]);
But I am trying to move this logic to a specific controller level so the stateChangeStart event will be fired only for specific views which are using this controller.
var payControllers = angular.module('pay.controllers', ['ui.bootstrap', 'ngIdle']);
payControllers.controller('payPaymentCtrl', ['$scope', '$state', '$window', '$location', 'payDataService', 'Idle', 'Keepalive', '$uibModal',
function ($scope, $state, $window, $location, service, Idle, Keepalive, $uibModal) {
var windowElement = angular.element($window);
windowElement.on('beforeunload', function (event) {
var path = $location.path();
if (path == '/payment' || path == '/paymentcreated') {
event.preventDefault();
}
});
$scope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams, options) {
//When payment or order page is refreshed, navigate to home
//Both payment and order share the same controller.
if (fromState.name == '' && (toState.name == 'payment' || toState.name == 'order')) {
$location.url('/');
}
});
$scope.paymentOption = service.GetpaymentOption();
if ($scope.paymentOption == null) {
//$state.go('home', {}, { reload: true });
//$location.path('/').replace();
$location.url('/');
}
}]);
The beforeunload event fires in the controller when a view is refreshed in browser but stateChangeStart does not fire.
Also, I tried to get rid of the stateChangeStart event from the controller level and handle to go to home page when some crucial data is missing. It gives me an error saying "Unable to load property of undefined". It appears to be trying to load some data that is bound to angular scope elements which usually appears upon some operation on home page. I need it to look like just the way when I open the home page for the first time when the application is run.
Thanks for any suggestions.

Related

ui-router $state.go based on user cookie

Is there a way to redirect a user to a specific state based on data from cookies when using ui-router?
I tried to do it from the .config() but since I'm not able to inject other dependencies it wasnt working.
I also tried to do it on the .run() block, but it just gives a loop (of course).
This is what I first tried on the .config()
function ConfigRouter($locationProvider, $stateProvider, $urlRouterProvider, Constant, localStorageService) {
$locationProvider.html5Mode(true);
$urlRouterProvider.when('/', function($injector) {
var $state = $injector.get('$state');
checkState($state);
});
$urlRouterProvider.when('', function($injector) {
var $state = $injector.get('$state');
checkState($state);
});
$urlRouterProvider.otherwise(function($injector) {
var $state = $injector.get('$state');
$state.go('error');
});
function checkState($state) {
var userCookie = localStorageService.cookie.get(Constant.cookieName);
if(userCookie.type == 1) {
$state.go('home-app');
} else if (userCookie.type == 2) {
$state.go('home-client');
} //and so on
}
}
Is there a way to do it? Or other way to achieve the same result? Basically I need to send the user to a different portion of the app based on the users role. If he is an admin, client, moderator, etc.. Each one has an specific app and need to retrieve specific data from server, this is why i want to do it, so i can request the data on the resolve of each state.
If you are using angular ui router, you can use resolve on the top state, there you can inject services which helps you to verify the cookie
you can also intercept and do it during
.run(["$rootScope", "$location", "$state", "services.userService", function ($rootScope, $location, $state) {
$rootScope.$on('$stateChangeStart', function (e, toState, toParams
, fromState, fromParams) {
// validation
});
more info https://github.com/angular-ui/ui-router/wiki#resolve
and examples here
angular ui-router login authentication
Defer Angular UI Router $stateChangeStart until server authorization response receieved
ej:
.state('main', {
templateUrl: 'app/modules/home/view.html',
abstract: true,
resolve: {
authorize: ['userService', "$state", "$q",
function (userService, $state, $q) {
return checkAuthorization(userService, $state, $q)
.then(function (user) {
return {user: user}
});
}]
},
controller: 'RootController'
})
// more states
var checkAuthorization = function(userService, $state){
//do all the necessary checks and return the user if he is already logged in
//redirecct to other page if the check failed
}
I did user check in the .run section:
.run(['$rootScope', '$state', '$cookies',
function ($rootScope, $state, $cookies) {
if ($cookies.get('token'))
$state.go('main');
else
$state.go('guest)
}]);
Of course than you should install 'token' in cookie. And now you don't need $urlRouterProvider in .config

Angular UI Router dynamic states on refresh goes to 404

I'm loading states dynamically based on the current user role. But when the page is refreshed it takes to the 404 page. Also in $stateChangeStart event fromState.name is blank.
Is there a way to go to the state before refresh button was clicked? Should I store the state before refresh is pressed and then use it?
.state('404', {
url: '/404',
templateUrl: '404.tmpl.html',
controller: function ($scope, $state, APP) {
$scope.app = APP;
$scope.goHome = function () {
$state.go('default.page');
};
}
})
$urlRouterProvider.otherwise('/404');
....
$rootScope.$on('$stateChangeStart', function (e, toState, toParams, fromState, fromParams) {
//fromState.name = '' on refresh
});
Thanks in advance!
The below seems to work, not sure if this is the best approach. On before unload event save the state, then use it in $stateChangeStart.
.run(function ($rootScope, $window, $state, authService, $http, $timeout, localStorageService) {
$rootScope.$on('$stateChangeStart', function (e, toState, toParams, fromState, fromParams) {
if (authService.isLoggedIn()) {
if (toState.name === "404" && fromState.name === '' && localStorageService.get("LAST_STATE") !== "404") {
authService.loadStates($stateProviderRef).then(function () {
$state.go(localStorageService.get("LAST_STATE"), localStorageService.get("LAST_STATE_PARAMS"));
});
}
}
});
window.onbeforeunload = function () {
localStorageService.set("LAST_STATE", $state.current.name);
localStorageService.set("LAST_STATE_PARAMS", $state.params);
return null;
}
})

How to pass url to $stateChangeStart of ui-router

I use the following to redirect routes to login if a user is not logged in:
angular.module('app').run(['$rootScope', '$location', '$state', 'AuthService',
function($rootScope, $location, $state, AuthService) {
/* redirect to login if not logged in */
$rootScope.$on( '$stateChangeStart',
function(e, toState, toParams, fromState, fromParams) {
if(toState.name === "login"){
return; // already going to login
}
if(!AuthService.user) {
e.preventDefault();
$state.go('login');
}
});
}]);
How can I pass the url from the "prevented" state to the login state so that I can continue navigating to that state once logged in?
Example: User clicks link for myapp.com/#/path/to/data --> redirects to myapp.com/#/login --> user logs in --> user routed to myapp.com/#/path/to/data
I basically need /path/to/data to be sent to my login controller so that I can handle that. I cannot seem to find '/path/to/data' in any of the state or param variables in the $stateChangeStart listener.
The $state service has an href method which can create a URL given the state and its parameters. Since those are provided to the state change listener, you can create a URL and then pass it as a param to the login state.
$rootScope.$on('$stateChangeStart',
function(e, toState, toParams, fromState, fromParams) {
if (toState.name === "login") {
return; // already going to login
}
if (!AuthService.user) {
e.preventDefault();
var redirectUrl = $state.href(toState.name, toParams);
$state.go('login', {
redirect: redirectUrl
});
}
});
You'll need to add a redirect parameter to your login state so you can use that.
You can store the pre-login location in $rootScope, and then fetch it from there once login is successful
if(!AuthService.user) {
e.preventDefault();
$rootScope.pre_login_path = $location.path();
$state.go('login');
}
And then after login, you can set the login path similarly in your login controller/service (have that include dependency injection of $rootScope and $location)
$location.path($rootScope.pre_login_path);

AngularJS: Get previous url (path) from page

I have a question: When I'm in a page I want return to previous page. I use the $routeProvider. How can I read the previous url?
I try to use this code in my controller but doesn't work...
angular.module.controller('myController',
function($scope, $routeParams, $http, $location, $rootScope) {
$rootScope.$on("$routeChangeSuccess",
function (event, current, previous, rejection) {
console.log(previous);
$location.path('PREVIOUS PATH');
});
});
How can I read the previous path? Thanks!
I am not fully sure, what you want to achieve. So I would suggest, check this before you go your own way:
How to implement history.back() in angular.js
But, in case, you want to know how to keep the last state with angular and UI-Router, we can do it with a service. There is some naive implementation tracking just last state (not challenging the history.back())
Check the working example
Service definition:
.factory('PreviousState', ['$rootScope', '$state',
function ($rootScope, $state) {
var lastHref = "/home",
lastStateName = "home",
lastParams = {};
$rootScope.$on("$stateChangeSuccess", function (event, toState, toParams
, fromState, fromParams) {
lastStateName = fromState.name;
lastParams = fromParams;
lastHref = $state.href(lastStateName, lastParams)
})
return {
getLastHref: function (){ return lastHref ; },
goToLastState: function (){ return $state.go(lastStateName, lastParams); },
}
}])
So we just do listen the $stateChangeSuccess and keep the track of last state name and its $stateParams.
We can inject our service to all scopes:
.run(['$rootScope', 'PreviousState',
function ($rootScope, PreviousState) {
$rootScope.PreviousState = PreviousState;
}])
And we can use it as a click or href:
<button ng-click="PreviousState.goToLastState()">go back</button>
<a ng-href="#{{PreviousState.getLastHref()}}" > go to last href</a>
Check that in action here

Unit testing angular-ui router maximum callstack

I'm new to angular-ui-router and I've been trying to do some unit testing for a basic authentication, its working fine until I hit a maximum call stack error.
I've narrowed the error down to the $state.go call in the app.run section.
I remove this and the test works. However it breaks my app
What can I do to resolve this? so I can test this section and make it work as well?
Why does this work normally but causes a test error?
Error:
RangeError: Maximum call stack size exceeded
at Scope.$broadcast (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular/angular.js:12876:15)
at Object.transitionTo (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2584:24)
at Object.go (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2454:21)
at /Users/paulrobinson/Workspace/contactCachePOC/dev/js/core.js:9:5889
at Scope.$broadcast (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular/angular.js:12874:28)
at Object.transitionTo (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2584:24)
at Object.go (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2454:21)
at /Users/paulrobinson/Workspace/contactCachePOC/dev/js/core.js:9:5889
at Scope.$broadcast (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular/angular.js:12874:28)
at Object.transitionTo (/Users/paulrobinson/Workspace/contactCachePOC/dev/bower_components/angular-ui-router/release/angular-ui-router.js:2584:24)
Code:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider){
$urlRouterProvider.otherwise('/');
var access = routingConfig.roles;
$stateProvider
.state('start', {
url : '/',
templateUrl : 'partials/decide.html',
controller : 'decideController',
data: {
access: access.anon
}
});
}]);
app.run(['$rootScope', '$state', 'AuthService', '$log', '$location',
function ($rootScope, $state, AuthService, $log, $location) {
$rootScope.$on("$stateChangeStart",
function (event, toState, toParams, fromState, fromParams) {
if (!AuthService.isAuthorized(toState.data.access)) {
event.preventDefault();
$rootScope.error = null;
//STATE.GO is causing the error
$state.go('start');
//$location.path('/#/');
return;
}
});
}]);
describe('Test as an anonymous user', function () {
var $templateCache, $state, $stateParams, $rootScope, $httpBackend,
AuthService,, $location;
var roles = {
anon: { id: 0, value: 'Public'},
user: { id: 1, value: 'User'}
};
beforeEach(module('app'));
beforeEach(inject(function(_$templateCache_, _$state_, _$stateParams_, _$rootScope_, _$httpBackend_,
_AuthService_, _sessionService_, _$location_) {
$templateCache = _$templateCache_;
$state = _$state_;
$stateParams = _$stateParams_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
AuthService = _AuthService_;
$location = _$location_;
//Fake it and say we're not authorized.
spyOn(AuthService, "isAuthorized").andCallFake(function (state){
return false;
});
}));
describe('View page.', function () {
beforeEach(function () {
$state.go('start', { });
$rootScope.$apply();
});
it('Should view page.', function () {
expect($state.current.name).toEqual('start');
});
});
});
Maximum call stack exceeded suggests that something is happening recursively. I'd guess that the state that you are going to "start" is also not authorised, so you get redirected to "start" again, and so on.
You need to make your AuthService.IsAuthorized return true at some point
Ok so it appears to be a bug with ui-router.
Here is a solution
https://github.com/angular-ui/ui-router/issues/178#issuecomment-49156829
$state.go(toState.name, toParams, {notify: false}).then(function() {
$rootScope.$broadcast('$stateChangeSuccess', toState, toParams, fromState, fromParams);
});
The above code will deal with the problem without breaking the browser or unit tests.
Just beware of https://github.com/angular-ui/ui-router/issues/1348 when using this fix.
Use $state.go("main", {}, {notify:false}); for notify to $stateChangeStart event.
$state.go("main", {}, {notify:false});

Resources