Here is what i have so far with ui-router states:
$stateProvider
.state('tools', {
url: '/tools/:tool',
template: '<div ui-view=""></div>',
abstract: true,
onEnter: function ($stateParams, $state, TOOL_TYPES) {
if (TOOL_TYPES.indexOf($stateParams.tool) === -1) {
$state.go('error');
}
}
})
.state('tools.list', {
url: '',
templateUrl: 'app/tools/tools.tpl.html',
controller: 'ToolsController'
})
.state('tools.view', {
url: '/:id/view',
templateUrl: 'app/tools/partials/tool.tpl.html',
controller: 'ToolController'
});
As you can see parent state has parameter tool which can be only in TOOL_TYPES array. So in case when tool is not available, i want to redirect to the error page.
Actually, everything works as expected, but i get two errors:
TypeError: Cannot read property '#' of null
TypeError: Cannot read property '#tools' of null
So i guess, child states have been 'hit' anyway. Is it possible to prevent this? Or maybe there is some other way to achieve what i want?
Angular ui-router's documentation mentions that onEnter callbacks gets called when a state becomes active, hence, the child states were activated.
To solve this problem you need to implement two things:
Create a resolve that returns a rejected promise once a specific condition does not apply to that state. Make sure that the rejected promise is passed with information regarding the state to redirect to.
Create a $stateChangeError event handler in the $rootScope and use the 6th parameter which is the representation of the information you have passed in the rejected promise. Use the information to create your redirect implementation.
DEMO
Javascript
angular.module('app', ['ui.router'])
.value('TOOL_TYPES', [
'tool1', 'tool2', 'tool3'
])
.config(function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('error', {
url: '/error',
template: 'Error!'
})
.state('tools', {
url: '/tools/:tool',
abstract: true,
template: '<ui-view></ui-view>',
resolve: {
tool_type: function($state, $q, $stateParams, TOOL_TYPES) {
var index = TOOL_TYPES.indexOf($stateParams.tool);
if(index === -1) {
return $q.reject({
state: 'error'
});
}
return TOOL_TYPES[index];
}
}
})
.state('tools.list', {
url: '',
template: 'List of Tools',
controller: 'ToolsController'
})
.state('tools.view', {
url: '/:id/view',
template: 'Tool View',
controller: 'ToolController'
});
})
.run(function($rootScope, $state) {
$rootScope.$on('$stateChangeError', function(
event, toState, toStateParams,
fromState, fromStateParams, error) {
if(error && error.state) {
$state.go(error.state, error.params, error.options);
}
});
})
.controller('ToolsController', function() {})
.controller('ToolController', function() {});
Related
I'm attempting to resolve some server-side data in an abstract parent state before the Home child state is loaded. I want to make sure I have the user's full name for examination in the state's data.rule function. However, using console.log statements, I can see the userFullName prints as an empty string to the console before the "done" statement in the factory.privilegeData method.
EDIT
The data in the parent resolve is security data from the server that needs to be available globally before any controllers are initialized. Only one child state is listed here (for the sake of readability), but all states, outside of login are children of the abstract parent. Once the parent's resolves are complete the data is stored in the $rootScope and used to determine access rights via the data.rule function in each state.
config.js
UPDATE: per answer below I've updated the parent/child states as such, howeve I'm still running into the same issue:
.state('app', {
url:'',
abstract: true,
template: '<div ui-view class="slide-animation"></div>',
resolve:{
privileges: ['$q', 'privilegesService', function($q, privilegesService){
console.log('from parent resolve');
return privilegesService.getPrivileges()
.then(privilegesService.privilegesData)
.catch(privilegesService.getPrivilegesError);
}]
}
})
.state('app.home', {
url: '/home',
templateUrl: 'app/components/home/home.html',
controller: 'HomeController',
controllerAs: 'vm',
parent: 'app',
resolvePolicy: {when:'LAZY', async: 'WAIT'},
authenticate: true,
data:{
rule: function($rootScope){
console.log("from home state data rule");
return true;
}
}
})
privilegesService.js
factory.getPrivileges = function () {
console.log('from server call');
var queryString = "&cmd=privilege&action=user";
return $http.get(config.serviceBaseUri + queryString);
};
factory.privilegesData = function(priv){
console.log('from privilege data');
if(priv && priv.data) {
$rootScope.userFullName = priv.data.firstName + ' ' + priv.data.lastName;
}
console.log('done');
};
Based on the console statments above I'm getting the following output. I need the from home state data rule to occur last...
...since I'm using the the results of the data.rule function for authorization for each state. Below is the $stateChangeStart from my .run method
app.route.js
$rootScope.$on("$stateChangeStart", function (event, toState, toParams, fromState, fromParams) {
if(toState.authenticate){
if(authService.authKeyExists()){
if(toState.data.rule($rootScope)){
navigationService.addNavObject("activity", {
summary : "Page navigation",
page : $location.absUrl().replace("#/", "")
});
} else {
$state.go('app.home');
}
} else {
event.preventDefault();
$state.go('login');
}
}
});
The child states need the parent state as a dependency in order for it to wait to load, even if its not used directly. Source
you have to try something like the following instead :
.state('app.home', {
url: '/home',
templateUrl: 'app/components/home/home.html',
controller: 'HomeController',
controllerAs: 'vm',
parent: 'app',
authenticate: true,
data:{
rule: function($rootScope){
console.log($rootScope.userFullName);
return true;
}
}
})
You can use the property resolvepolicy and set it to LAZY as well, a state's LAZY resolves will wait for the parent state's resolves to finish before beginning to load.
I am having a lot of trouble determining how to add permissions / access-control to our Angular app. Right now we have this:
app.config(['$stateProvider', function ($stateProvider) {
$stateProvider
.state('default', {
url: '',
templateUrl: 'pages/home/view/home.html',
controller: 'HomeCtrl'
})
.state('home', {
url: '/home',
templateUrl: 'pages/home/view/home.html',
controller: 'HomeCtrl',
permissions: { // ?
only: ['Admin','Moderator']
},
access: { // ?
roles: ['*']
},
resolve: { // ?
authenticate: function($state, $q, $timeout){
},
}
})
}]);
I having trouble determining which methodology to use to create access control to each page.
Right now, the logged in user is stored in an Angular value:
app.value('USER', JSON.parse('{{{user}}}'));
The USER value contains the information about which roles / permissions the user has.
But I cannot inject USER into app.config() callback, it says "unknown provider".
How can I do access-control based of the USER parameters?
the key to perform that is to add your access control on an event $stateChangeStart
For example if you have your routing like that :
.state('termsofuse', {
url: "/terms",
templateUrl: "termOfUse.html",
resolve: {
authorizedRoles: function() {
return [USER_ROLES['su'],
USER_ROLES['user'],
USER_ROLES['admin'],
USER_ROLES['skysu']
]
}
}
})
you may define your access controle like that
.run(
function($rootScope, $q, $location, $window, $timeout) {
$rootScope.$on(
'$stateChangeStart',
function(event, next, current) {
var authorizedRoles = next.resolve.authorizedRoles();
//this function controls if user has necessary roles
if (!isAuthorized(authorizedRoles)) {
event.preventDefault();
// and I broadcast the news $rootScope.$broadcast("AUTH_EVENTS.notAuthenticated");
} else {
$rootScope.$broadcast("AUTH_EVENTS.loginSuccess");
}
})
});
Then you just of to define your event's catcher to manager the desired behaviour (redirect / error message or whatever necessary)
You might want to check out ngAA, I use it alongside ui-router.
This is how I got it to work. I am not sure if it's the best way, but it does work.
You cannot inject services/values in the app.config(), but you can inject services/values into the resolve.authenticate function.
I decided to use the resolve.authenticate methodology.
.state('home', {
url: '/home',
templateUrl: 'pages/home/view/home.html',
controller: 'HomeCtrl',
resolve: {
authenticate: handlePageRequest['home'] // <<<<
}
})
and then we have a handler:
let handlePageRequest = {
'default': function ($q, USER, $state, $timeout, AuthService) {
// this page is always ok to access
return $q.when();
},
'home': function ($q, USER, $state, $timeout, AuthService) {
if(AuthService.isHomePageAccessible(USER)){
return $q.when();
}
else{
$timeout(function () {
$state.go('not-found'); // we can prevent user from accessing home page this way
}, 100);
return $q.reject()
}
},
};
I'm trying to build some sort of authentication in my angular app and would like to redirect to a external URL when a user is not logged in (based on a $http.get).
Somehow I end up in an infinite loop when the event.preventDefault() is the first line in the $stateChangeStart.
I've seen multiple issues with answers on stackoverflow, saying like "place the event.preventDefault() just before the state.go in the else". But then the controllers are fired and the page is already shown before the promise is returned.
Even when I put the event.preventDefault() in the else, something odd happens:
Going to the root URL, it automatically adds the /#/ after the URL and $stateChangeStart is fired multiple times.
app.js run part:
.run(['$rootScope', '$window', '$state', 'authentication', function ($rootScope, $window, $state, authentication) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams) {
event.preventDefault();
authentication.identity()
.then(function (identity) {
if (!authentication.isAuthenticated()) {
$window.location.href = 'external URL';
return;
} else {
$state.go(toState, toParams);
}
});
});
}]);
authentication.factory.js identity() function:
function getIdentity() {
if (_identity) {
_authenticated = true;
deferred.resolve(_identity);
return deferred.promise;
}
return $http.get('URL')
.then(function (identity) {
_authenticated = true;
_identity = identity;
return _identity;
}, function () {
_authenticated = false;
});
}
EDIT: Added the states:
$stateProvider
.state('site', {
url: '',
abstract: true,
views: {
'feeds': {
templateUrl: 'partials/feeds.html',
controller: 'userFeedsController as userFeedsCtrl'
}
},
resolve: ['$window', 'authentication', function ($window, authentication) {
authentication.identity()
.then(function (identity) {
if (!authentication.isAuthenticated()) {
$window.location.href = 'external URL';
}
})
}]
})
.state('site.start', {
url: '/',
views: {
'container#': {
templateUrl: 'partials/start.html'
}
}
})
.state('site.itemList', {
url: '/feed/{feedId}',
views: {
'container#': {
templateUrl: 'partials/item-list.html',
controller: 'itemListController as itemListCtrl'
}
}
})
.state('site.itemDetails', {
url: '/items/{itemId}',
views: {
'container#': {
templateUrl: 'partials/item-details.html',
controller: 'itemsController as itemsCtrl'
}
}
})
}])
If you need more info, or more pieces of code from the app.js let me know !
$stateChangeStart will not wait for your promise to be resolved before exiting. The only way to make the state wait for a promise is to use resolve within the state's options.
.config(function($stateProvider) {
$stateProvider.state('home', {
url: '/',
resolve: {
auth: function($window, authentication) {
return authentication.identity().then(function (identity) {
if (!authentication.isAuthenticated()) {
$window.location.href = 'external URL';
}
});
}
}
});
});
By returning a promise from the function, ui-router won't initialize the state until that promise is resolved.
If you have other or children states that need to wait for this, you'll need to inject auth in.
From the wiki:
The resolve keys MUST be injected into the child states if you want to wait for the promises to be resolved before instantiating the children.
This is a common issue, I see it a ton but nothing seems to work. Here is what I'm doing. I want to have some dynamic animations with my states, so basically login will do some cool animations and move into the actual interface. Now, I started nesting the views like this:
$stateProvider
.state('login', {
url: '/login',
title: "Login",
views: {
"master": {
controller: "LoginController",
templateUrl: "/components/login/login.html",
authentication: false
}
}
})
.state("logout", {
url: "/logout",
title: "Logout",
authentication: false
})
.state('foo', {
url: '/',
controller: "HomeController",
views: {
"master": {
templateUrl: '/components/ui-views/masterView.html'
},
"sidebar#foo": {
templateUrl: '/components/sidebar/_sidebar.html'
},
"header#foo": {
templateUrl: '/components/header/_header.html'
}
}
})
.state('foo.inventory', {
url: '/inventory',
title: "Inventory",
views: {
"content#foo": {
controller: "InventoryController",
templateUrl: "/components/inventory/inventory.html"
}
}
});
So while running this I need to redirect to logout, but it gets stuck. It won't move from this logout state at all.
Here is how I'm handling that:
function run($http, $rootScope, $cookieStore, $state, $templateCache, $timeout, AuthService, modalService, const) {
var timeout;
FastClick.attach(document.body);
$rootScope.globals = $cookieStore.get('globals') || {};
if ($state.current.name !== 'login' && !$rootScope.globals.guid) {
$state.go('login');
} else if ($state.current.name == 'login' && $rootScope.globals.guid) {
$state.go('foo');
}
$rootScope.$on('$stateChangeStart', function (event, next, current, fromState, fromParams) {
var authorizedRoles = next.level != undefined ? next.level : 1,
needAuth = next.authentication != undefined ? next.authentication : true;
if (needAuth) {
if (!AuthService.isAuthorized(authorizedRoles, true)) {
event.preventDefault();
if (AuthService.isAuthenticated()) {
$rootScope.$broadcast(const.auth.notAuthorized);
$state.go('foo', {});
} else {
$rootScope.$broadcast(const.auth.notAuthenticated);
}
}
}
if (next.name == 'logout') AuthService.logout($rootScope.globals);
});
}
So why would this not work? It seems like this work fine. But the $state.go('login') returns a bad value.
If anyone could guide me in the right direction, or tell me what is wrong exactly.
The issue is that the $state.go is in the run, there doesn't seem to be many docs on this topic. $state.go is not initialized yet and will not run.
I had the same problem. After debugging into how the attribute ui-sref works for hyperlinks, i found out that the $state.go is wrapped around $timeout in the core angular-ui library.
var transition = $timeout(function() {
debugger;
$state.go("app.authentication");
});
Perhaps you could try the same. (Although the ui-sref event handler in the core library clearly mentions that this is a hack.)
只需要升级ui-router版本就可以了,见连接地址:https://github.com/mattlewis92/angular-bluebird-promises/issues/11
If you upgrade to ui-router 0.3.2 it's actually been fixed there: angular-ui/ui-router#66ab048
I have faced this issues in my app, you are trying to call $state.go on an internal ui-view that is not relative to the state ,thats you are facing this problem, so try this :
app.run(['$state', '$rootScope', '$timeout',
function ($state, $rootScope, $timeout) {
$timeout(function() {
$state.go('login');
});
}]);
I'm a beginner with AngularJS and ui-router, and am trying to handle 404s on resources not found. I would like an error to be displayed, without changing the URL in the address bar.
I have configured my states as such:
app.config([
"$stateProvider", function($stateProvider) {
$stateProvider
.state("home", {
url: "/",
templateUrl: "app/views/home/home.html"
})
.state("listings", {
abstract: true,
url: "/listings",
templateUrl: "app/views/listings/listings.html"
})
.state("listings.list", {
url: "",
templateUrl: "app/views/listings/listings.list.html",
})
.state("listings.details", {
url: "/{id:.{36}}",
templateUrl: "app/views/listings/listings.details.html",
resolve: {
listing: [
"$stateParams", "listingRepository",
function($stateParams, repository) {
return repository.get({ id: $stateParams.id }).$promise;
}
]
}
})
.state("listings.notFound", {
url: "/404",
template: "Listing not found"
});
}
]);
(I'm actually using TypeScript, but I tried to change the above to be pure JavaScript)
If, say, I navigate to the following url:
http://localhost:12345/listings/bef8a5dc-0f9e-4541-8446-4ebb10882045
That should open the listings.details state.
However if that resource does not exist, the promise returned from the resolve function will fail with a 404, which I catch here:
app.run([
"$rootScope", "$state",
function($rootScope, $state) {
$rootScope.$on("$stateChangeError", function(event, toState, toParams, fromState, fromParams, error) {
event.preventDefault();
if (error.status === 404) {
$state.go("^.notFound", null, { location: true, relative: toState });
}
});
}
]);
What I'm trying to do here is to go to the listings.notFound state, without changing the destination URL in the address bar.
I use a relative path because I would like to re-use this logic for other resources.
However, I get an exception:
Path '^.notFound' not valid for state 'listings.details'
This error happens because the toState argument given by the $stateChangeError event does not know its parent, i.e. toState.parent is undefined.
In the transitionTo function of ui-router, I can see that the object given as argument is to.self, which only provides a subset of the information.
Using relative: $state.get(toState.name) also doesn't help, because internally ui-router once again returns state.self
I would like to avoid maintaining a list of absolute paths, and rewrite the logic for navigating in the state hierarchy (no matter how simple it is, DRY and all that).
Am I going about it wrong, is there another proper way of handling 404?
If not, what is the best approach?
It's better to use $urlRouterProvider to handle this exceptions
app.config([
"$stateProvider", "$urlRouterProvider", function ($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise('/404');
// define states
}]);