So I have these routes set up:
.state('createOrder', {
url: '/customer-services/orders/create',
templateUrl: 'tpl/customerServices/orders/save.html',
controller: 'SaveOrderController',
controllerAs: 'controller',
resolve: {
order: ['SaveOrderService', function (shared) {
shared.order = { forDelivery: true };
}]
},
data: {
requireLogin: true,
pageTitle: 'Add order'
}
}).state('createOrder.lines', {
url: '/lines',
views: {
'#': {
templateUrl: 'tpl/customerServices/orders/save/line.html',
controller: 'SaveOrderLinesController',
controllerAs: 'controller'
}
},
resolve: {
validate: ['$state', 'SaveOrderService', function ($state, shared) {
// If we don't have an account number
if (!shared.order.accountNumber) {
console.log('redirecting');
// Redirect to the create order view
$state.go('createOrder');
}
}]
},
data: {
requireLogin: true,
pageTitle: 'Add order : Lines'
}
})
But the state does not change. I thought that there might be an error somewhere, so I subscribed the the state events like this:
// On state change
$rootScope.$on('$stateChangeStart', function (event, toState) {
var data = toState.data; // Get our state data
var requireLogin = typeof data === 'undefined' ? false : data.requireLogin; // Check to see if we have any data and if so, check to see if we need login rights
var user = service.get(); // Get our current user
console.log(toState);
$rootScope.currentUser = user; // Set our current user on the rootScope
// If we require login rights and we are not authenticated
if (requireLogin && !user.authenticated) {
event.preventDefault(); // Stop processing
$state.transitionTo('login'); // And redirect to the login page
}
});
$rootScope.$on('$stateNotFound', function () {
console.log('state not found');
});
$rootScope.$on('$stateChangeError', function () {
console.log('state errored');
});
$rootScope.$on('$stateChangeSuccess', function () {
console.log('state changed');
});
and when I refresh my lines view, the console outputs this:
Object { url: "/lines", views: Object, resolve: Object, data: Object, name: "createOrder.lines" } app.js:42:9
redirecting app.js:2436:21
Object { url: "/customer-services", templateUrl: "tpl/customerServices/index.html", controller: "CustomerServicesController", controllerAs: "controller", data: Object, name: "customerServices" } app.js:42:9
state changed app.js:63:9
Object { order: Object, account: null, collectionPoint: null }
As you can see, the states think they have changed, but I still see the createOrder.lines view.
Does anyone have any idea why?
I think you'll need to wrap the $state change in a function that will trigger a digest cycle whilst also rejecting the promise in the resolve method...
$timeout(function() { $state.go("createOrder") });
return $q.reject("Rejection message!");
Remember to inject $timeout and $q into your resolve function! =)
Should also add that rejecting the resolve will fire stateChangeError.
So, it turns out you don't need the promise. Just adding the timeout works.
I found another post which suggests that the timeout is needed to avoid digest issues (which I am guessing is what is causing my states to not change).
Here is the final code:
validate: ['$state', '$timeout', 'SaveOrderService', function ($state, $timeout, shared) {
// If we don't have an account number
if (!shared.order.accountNumber) {
// Timeout to avoid digest issues
$timeout(function () {
// Redirect to the create order view
$state.go('createOrder');
});
}
}]
Related
I want to verify if the user can access a state before he gets there, if he doesn't have permissions will be redirected to another page.
The problem is that I'm doing a SPA and it verifies the permissions, but it takes a while until the server send the response and the user is redirected, so what happen is that a screen appears for 1 or 2 seconds and then is redirected successfully. Is there anyway to avoid this?
This is the code for the state change:
webApp.run(function ($rootScope, $state, StateService) {
$rootScope.$on('$stateChangeStart', function (event, toState, fromState, toParams) {
StateService.hasAccessTo(toState.name, function(data){
if (data.data != ""){
event.preventDefault();
$state.go(data.data);
}
});
});
});
and the service:
webApp.service('StateService', function($http, $rootScope){
this.hasAccessTo = function(state, callback){
$http.get("state/" + state).then(callback);
}
});
I have also tried with a promise in the $stateChangeStart, but it didn't work.
I read about interceptors, but they work if the user is in another page and access mine, if he is already on the page and type a link manually it doesn't intercepts.
Any modifications or suggestions of new ideas or improvements are welcome!
EDIT
Now I have this:
var hasAccessVerification = ['$q', 'StateService', function ($q, $state, StateService) {
var deferred = $q.defer();
StateService.hasAccessTo(this.name, function (data) {
if (data.data !== '') {
$state.go(data.data);
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}];
$urlRouterProvider.otherwise("/");
$compileProvider.debugInfoEnabled(false);
$stateProvider
.state('welcome',{
url:"/",
views: {
'form-view': {
templateUrl: '/partials/form.html',
controller: 'Controller as ctrl'
},
'#': {
templateUrl: '/partials/welcome.html'
}
},
data: {
requireLogin: false
},
resolve: {
hasAccess: hasAccessVerification
}
})
And it validates, but it doesn't load the template. It doesn't show de views. What might I be doing wrong?
EDIT 2
I forgot to add $state here:
var hasAccessVerification = ['$q', '$state', 'StateService', function ($q, $state, StateService){...}
Consider using the resolve in your state configuration instead of using $stateChangeStart event.
According to the docs:
If any of these dependencies are promises, they will be resolved and
converted to a value before the controller is instantiated and the
$stateChangeSuccess event is fired.
Example:
var hasAccessFooFunction = ['$q', 'StateService', function ($q, StateService) {
var deferred = $q.defer();
StateService.hasAccessTo(this.name, function (data) {
if (data.data !== '') {
$state.go(data.data);
deferred.reject();
} else {
deferred.resolve();
}
});
return deferred.promise;
}];
$stateProvider
.state('dashboard', {
url: '/dashboard',
templateUrl: 'views/dashboard.html',
resolve: {
hasAccessFoo: hasAccessFooFunction
}
})
.state('user', {
abstract: true,
url: '/user',
resolve: {
hasAccessFoo: hasAccessFooFunction
},
template: '<ui-view/>'
})
.state('user.create', {
url: '/create',
templateUrl: 'views/user/create.html'
})
.state('user.list', {
url: '/list',
templateUrl: 'views/user/list.html'
})
.state('user.edit', {
url: '/:id',
templateUrl: 'views/user/edit.html'
})
.state('visitors', {
url: '/gram-panchayat',
resolve: {
hasAccessFoo: hasAccessFooFunction
},
templateUrl: 'views/visitor/list.html'
});
And according to the docs https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#inherited-resolved-dependencies resolve are inherited:
New in version 0.2.0
Child states will inherit resolved dependencies from parent state(s),
which they can overwrite. You can then inject resolved dependencies
into the controllers and resolve functions of child states.
But, please note:
The resolve keyword MUST be on the state not the views (in case you
use multiple views).
The best practice is to have interceptor on responseError which checks the response status and acts accordingly:
webApp.config(['$httpProvider' ($httpProvider) {
var interceptor = ['$q', '$rootScope', function ($q, $rootScope) {
return {
request: function (config) {
// can also do something here
// for example, add token header
return config;
},
'responseError': function (rejection) {
if (rejection.status == 401 && rejection.config.url !== '/url/to/login') {
// If we're not on the login page
$rootScope.$broadcast('auth:loginRequired');
}
}
return $q.reject(rejection);
}
}
}];
$httpProvider.interceptors.push(interceptor);
}]);
And handle redirection in run block
webApp.run(['$rootScope', function($rootScope){
$rootScope.$on('auth:loginRequired', function () {
$state.go('loginState');
});
}]);
The good thing is that $state service does not need to deal with permission logic:
$stateProvider
.state('someState', {
url: '/some-state',
templateUrl: '/some-state.html',
resolve: {
dataFromBackend: ['dataService', function (postingService) {
// if the request fails, the user gets redirected
return dataService.getData();
}],
},
controller: function ($scope, dataFromBackend) {
}
})
Notice
With this approach, you do not need StateService, all you need to do is to return proper response statuses from backend. For example, if the user is guest, return 401 status.
When the user hit F5 or refresh button I need to call a function before refreshing the page, I tried the below code and it did'nt work, please suggest me what I'm doing wrong and is there a better way to do this.
I'm using Anuglar 1.5 with ui-router.
angular.module('module.name', [
]).controller('someController',['$scope', function($scope) {
$scope.$on("$stateChangeStart", function () {
functionToCallOnPageRefresh();
});
}]);
functionToCallOnPageRefresh() is not getting called on page refresh.
you need to create a parent state like as 'secure' and inherit every state in your application with that state like as-
angular.module('moduleName').config(['$stateProvider', '$urlRouterProvider',
function ($stateProvider, $urlRouterProvider) {
$stateProvider.
state('secure', {
url: "/",
abstract: true,
templateUrl: "/path/to your/ master page",
resolve: {
factory: 'CheckRouting'
}
}).
state('dashboard', {
url: 'dashboard',
templateUrl: 'path/to your /template',
parent: 'secure',
});
}
]);
here i have mentioned
resolve: {
factory: 'CheckRouting'
}
in which CheckRouting inside resolve property, is a factory which is going to do some task (check user is login or not) on sate change or press f5.
in 'secure' state use 'resolve' property to execute a function if user press f5 or state change like as-
(function() {
'use strict';
angular.module('moduleName').factory('CheckRouting', ['$rootScope','$timeout' checkRouting]);
function checkRouting($rootScope,$timeout) {
if ( condition) { //user is loged in
return true;
} else {
$timeout(function() {
$state.go('login');
});
return false;
}
}
}());
I'm developing an Angular application. In this, I'm authenticating the user before going to dashboard. To achieve this I have wrote the signIn function as
Sign-In Function
this.signIn = function(credentials) {
console.info('AccountController[signIn] Called');
AuthService
.login(credentials)
.then(function(authenticatedUser) {
$scope.globals['currentUser'] = authenticatedUser;
AuthService.setCurrentUser(authenticatedUser);
$scope.globals['isAuthenticated'] = true;
$location.path('/dashboard');
}).catch(function(error) {
console.warn('AccountController[signIn] :: ', error);
Flash.Error(error);
$scope.credentials.password = '';
});
};
I also want to restrict the user from accessing the routes, if they are not logged in. To achieve that I came up with this dirty code.
Routes
$stateProvider
.state('signIn', {
url: '/signIn',
templateUrl: 'partials/signIn/signIn.html',
data: {
pageTitle: 'SignIn'
},
controller: 'AccountController',
controllerAs: 'ac',
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var userInfo = AuthService.isAuthenticated();
console.info('SignIn Route[isAuthenticated] :: ', userInfo);
if (!userInfo) {
return $q.when(userInfo);
} else {
return $q.reject({
isAuthenticated: true
});
}
}]
}
})
.state('dashboard', {
url: '/dashboard',
templateUrl: 'partials/dashboard.html',
controller: 'DashboardController',
access: {
requiredLogin: true
},
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
console.info('dashboard Route[isAuthenticated] :: ', authenticated);
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
})
.state('manageStudent', {
url: '/manageStudent',
templateUrl: 'partials/manageStudent.html',
access: {
requiredLogin: true
},
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
});
App.run(['$rootScope', 'settings', '$state', 'AuthService', '$location', function($rootScope, settings, $state, AuthService, $location) {
$rootScope.$state = $state; // state to be accessed from view
$rootScope.$settings = settings; // state to be accessed from view
$rootScope.$on('$stateChangeStart', function(event, next,nextParams,prev,prevParams) {
// If the user is logged in don't allow him to land on the Login Page
if (next.access !== undefined) {
if (next.access.requiredLogin && !AuthService.isAuthenticated()) {
$location.path('/signIn');
}
}
});
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
event.preventDefault();
if (!error.isAuthenticated) {
console.warn("I'm not Authenticated.Going to Sign-in");
return $location.path('/signIn');
} else {
console.info("I'm Authenticated");
$location.path('/dashboard');
}
});
}]);
Reason I said the above code DIRTY is because, If I have 10 routes which I want to protect from Unauthenticated user, I have to copy the same resolve function in all the routes.
So my question is , what should I do to get rid of multiple resolve function and being able to write DRY code?
Since auth should be resolved on each route change, it is insufficient to just wrap it into separate factory (which is a singleton and will run only once). To get round this limitation it should be a function
app.factory('authResolver', function ($q, AuthService) {
return function () {
// ...
};
});
which runs on every route resolve
...
resolve: {
auth: function (authResolver) {
return authResolver();
}
}
Still not that DRY, but that's the recommended humidity level.
More radical approach that may save the one from boilerplate resolve and save a few lines of code will be similar to that:
app.run(function ($rootScope, authResolver) {
$rootScope.$on('$stateChangeStart', function (e, to) {
if (to.doAuthPlease)
to.resolve.auth = authResolver();
});
});
and
...
doAuthPlease: true,
resolve: {}
The obvious difference with ngRoute in the mentioned answer is that in UI Router you need to have resolve object defined to be able to add new resolvers to the state dynamically. It can be treated like that or leaved as is.
You're on the right track so far. You have what looks like a custom data member access: { requiredLogin: true} on your state objects.
The next step is to use this with the State Change Events that ui-router provides:
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
if (toState.access.requiredLogin) {
if(!AuthService.isAuthenticated()) {
event.preventDefault();
// redirect to signIn?
}
}
});
This would be placed in your .run block somewhere which means AuthService needs to be injected there as well. This should remove the need for the resolve block on every route.
Hope that helps.
Update:
if your AuthService.isAuthenticated() function returns a promise, it could be potentially dangerous to rely on the promise to resolve within the event handler (it may move on before the promise resolves). Its probably better that you run the AuthService function before the block (as the application starts) and then store it in a variable:
var isAuth;
AuthService.isAuthenticated().then(function (result) { isAuth = result });
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState) {
if (toState.access.requiredLogin) {
if(!isAuth) {
event.preventDefault();
// redirect to signIn?
}
}
});
var $delegate = $stateProvider.state;
$stateProvider.state = function(name, definition) {
var unrestricted = ['signIn'];
if (unrestricted.indexOf(name) === -1) {
definition.resolve = angular.extend({}, definition.resolve, {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
});
}
return $delegate.apply(this, arguments);
};
Here I'm dynamically adding the resolve to the routes which I want to restrict.
Because you're using ui.router states (and assuming you're using v0.2.0 or greater), you can use state inheritance to solve this with the resolve and not have to duplicate it all over your various states.
What Do Child States Inherit From Parent States?
Child states DO inherit the following from parent states:
Resolved dependencies via resolve
Custom data properties
Nothing else is inherited (no controllers, templates, url, etc).
Inherited Resolved Dependencies
New in version 0.2.0
Child states will inherit resolved dependencies from parent state(s), which they can overwrite. You can then inject resolved dependencies into the controllers and resolve functions of child states.
src - https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#what-do-child-states-inherit-from-parent-states
I accomplish this by using an abstract base state that will defined essentially the same thing you're doing, checking to see if the user is allowed to proceed. Since all of my UI states inherit from the abstract parent state, the authentication dependency is resolved for each of them.
abstract base state
.state('baseState', {
url: '',
abstract: true,
template: '<ui-view></ui-view>'
resolve: {
auth: ['$q', 'AuthService', function($q, AuthService) {
var authenticated = AuthService.isAuthenticated();
console.info('dashboard Route[isAuthenticated] :: ', authenticated);
if (authenticated) {
return $q.when(authenticated);
} else {
return $q.reject({
isAuthenticated: false
});
}
}]
}
})
other states
.state('dashboard', {
parent: 'baseState'
url: '/dashboard',
templateUrl: 'partials/dashboard.html',
controller: 'DashboardController',
...
})
I'm trying to create a parent state in my Route definition to handle Autorization like this:
$stateProvider.state('root', {
abstract: true,
resolve: {
auth: ['Auth',
function (Auth) {
return Auth.authorize()
}
]}
})
this parent state is defined in my other states like this:
.state('home', {
parent: 'root',
url: '/',
templateUrl: 'views/home.html',
controller: 'HomeCtrl'
})
From my understanding of parent states and this post it should work just like this.
The Auth.authorize method works fine, since I can do a simple resolve on each state. Here's the function:
function authorize() {
var user = getCurrentUser();
if (user) return $q.when(user);
else {
$timeout(function() {
$state.go('login')
});
return $q.reject();
}
}
The response is ok, but the page doesn't get displayed. What am I missing here?
I have two states, one is a child of the other. One represents a list of people (people) and one for when you click on an individual person to view more details (people.detail).
My first state works as intended, and has several parameters which represent all the various server side filters and paging you could apply. The child state is a modal window, which popups as expected but my only paramater personID never makes it into $stateParams. I wonder if it's something to do the combination of the RESTful style URL and the query string style?
It is perhaps worth noting that $stateParams is populated with everything you'd expect from the parent state.
EDIT: Plunker to show what I mean - http://plnkr.co/edit/eNMIEt?p=info (note that the ID is undefined)
app.js
$urlRouterProvider.otherwise('/people');
$stateProvider
.state('people', {
url: '/people?pageNumber&pageSize&sortField&sortDirection&search&countryID&jobFunctionIDs&firmTypeIDs',
templateUrl: 'Static/js/angular/views/people-list.html',
controller: 'PeopleListController',
resolve: {
api: "api",
people: function (api, $stateParams) {
//Code ommitted
},
countries: function (api) {
//Code ommitted
},
jobFunctions: function (api) {
//Code ommitted
},
firmTypes: function (api) {
//Code ommitted
}
}
});
modalStateProvider.state('people.detail', {
url: "/{personID}",
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function () {
},
resolve: {
person: function (api, $stateParams) {
return api.people.getDetail($stateParams.personID);
}
}
});
The modalStateProvider looks like:
angular.module('myApp')
.provider('modalState', function ($stateProvider) {
var provider = this;
this.$get = function () {
return provider;
}
this.state = function (stateName, options) {
var modalInstance;
$stateProvider.state(stateName, {
url: options.url,
onEnter: function ($modal, $state) {
modalInstance = $modal.open(options);
modalInstance.result['finally'](function () {
modalInstance = null;
if ($state.$current.name === stateName) {
$state.go('^');
}
});
},
onExit: function () {
if (modalInstance) {
modalInstance.close();
}
}
});
};
})
And finally my function in my controller to transition to the people.detail state:
$scope.transitionToPersonDetail = function (personID) {
$state.transitionTo('.detail', { personID: personID }, { location: true, inherit: true, relative: $state.$current, notify: false });
};
After a lot more inspection I'm still not entirely sure why this was happening, I think it had something to do with the modalStateProvider's scope with $stateParams and the fact that the state wasn't "ready". All of this is purely speculation however.
I fixed it with this code:
$stateProvider.state('people.detail', {
url: '/{personID:int}',
onEnter: ['$stateParams', '$state', '$modal', 'api', function($stateParams, $state, $modal, api) {
$modal.open({
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function(person) {
console.log(person);
},
resolve: {
person: function() {
return api.people.getDetail($stateParams.personID);
}
}
}).result['finally'](function(result) {
$state.transitionTo('people');
});
}]
});
As far as I remember the value from 'resolve' is directly available in controller, although I think it's worth checking if controller for your child view is triggered at all
modalStateProvider.state('people.detail', {
url: "/:personID",
templateUrl: 'Static/js/angular/views/people-detail.html',
controller: function () {
console.log(person)
},
resolve: {
person: function (api, $stateParams) {
return api.people.getDetail($stateParams.personID);
}
}
});