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');
});
}]);
Related
I am using ui-router for routing in my angularjs app and ui-bootstrap for UI.In my app on entering a state i am opening a uibmodal which basically returns a uibmodalinstance but when i change a state using
$state.go('dashboard')
Inside my controller it is changing the state but didn't closing modal.
So i want modal to be closed on exiting the state.
i Have written following code but some part of code doesn't work.
please see coding and the comments for not working part
$stateProvider.state('makeabid',{
parent: 'dashboard',
url: '/makeabid/{id}',
data: {
authorities: ['ROLE_USER'],
pageTitle: 'global.menu.makeabid'
},
onEnter: ['$stateParams', '$state', '$uibModal', function($stateParams, $state, $uibModal) {
$uibModal.open({
templateUrl: 'app/dashboard/makeabid/makeabid.html',
controller: 'MakeabidController',
controllerAs: 'vm',
backdrop: true,
size: 'lg'
}).result.then(function () {
$state.go('dashboard');
});
}]
//this part doesnt work
,onExit:['$uibModalInstance','$stateParams', '$state',function ($uibModalInstance,$stateParams, $state) {
$uibModalInstance.close();
}]
});
My Controller Coding is as follows : -
MakeabidController.$inject = ['$stateParams','$state','$uibModalInstance','MakeabidService'];
function MakeabidController( $stateParams, $state, $uibModalInstance, MakeabidService) {
var vm = this;
loadAll();
vm.clear = clear;
vm.save = save;
function clear () {
$uibModalInstance.close();
}
function save() {
// console.log(vm.comparableData);
}
function loadAll() {
vm.comparableData = MakeabidService.getobject();
if(angular.isUndefined(vm.comparableData)){
//$uibModalInstance.close(); //It doesn't work
$state.go('dashboard'); //This is working
}
}
}
AnyOne Please Tell me solution for closing the uibmodal on changing state
I solved it by adding $uibModalStack.close() in my app.run
function run($uibModalStack) {
$rootScope.$on('$stateChangeStart', function() {
$uibModalStack.dismissAll();
});
}
You can tap into to some of the $stateProvider events.
I do something similar in one of my apps (in coffeescript, but you get the idea)
#$scope.$on '$stateChangeStart', (e, to, top, from, fromp) =>
#$uibModalInstance.close()
Basically, in your controller that handles the modal, you will watch for the $stateChangeStart event, and when you catch it, you can close the modal.
See https://github.com/angular-ui/ui-router/wiki, specifically the section on State Change Events
---EDIT---
I just noticed that these calls are deprecated. If you are using UI-Router > 1.0, there is some documentation here on how to migrate: https://ui-router.github.io/guide/ng1/migrate-to-1_0#state-change-events
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()
}
},
};
The use case is to change login button to text "logged in as xxx" after authentication.
I have devided my page to 3 views: header, content, footer. The login button is in the header view. When I click login, it transits to "app.login" state, and the content view changes to allow user input username and password.
Here's the routing code:
app.config(['$stateProvider', '$urlRouterProvider',
function($stateProvider, $urlRouterProvider) {
$stateProvider
.state('app', {
url: '/',
views: {
'header': {
templateUrl: 'static/templates/header.html',
controller: 'AppController'
},
'content': {
templateUrl: 'static/templates/home.html',
controller: 'HomeController'
},
'footer': {
templateUrl: 'static/templates/footer.html',
}
}
})
.state('app.login', {
url: 'login',
views: {
'content#': {
templateUrl : 'static/templates/login.html',
controller : 'LoginController'
}
}
})
The html template has code like this:
<li><span ng-if='loggedIn' class="navbar-text">
Signed in as {{currentUser.username}}</span>
</li>
LoginController set a $scope.loggedIn flag to true once authentication succeeded, but how can I populate that flag to the header view?
As I understand it I can't just use $scope.loggedIn in the html template as above because the $scope is different in two controllers. I know if LoginController is a child of AppController, then I can call $scope.$emit in LoginController with an event and call $scope.$on in AppController to capture it. But in this case the two controllers are for different views, how can I make them parent-child?
I know I can use $rootScope but as I'm told polluting $rootScope is the last resort so I'm trying to find a best practise. This must be a very common use cases so I must be missing something obvious.
You can use a factory to handle authentication:
app.factory( 'AuthService', function() {
var currentUser;
return {
login: function() {
// logic
},
logout: function() {
// logic
},
isLoggedIn: function() {
// logic
},
currentUser: function() {
return currentUser;
}
};
});
Than can inject the AuthService in your controllers.
The following code watches for changes in a value from the service (by calling the function specified) and then syncs the changed values:
app.controller( 'AppController', function( $scope, AuthService ) {
$scope.$watch( AuthService.isLoggedIn, function ( isLoggedIn ) {
$scope.isLoggedIn = isLoggedIn;
$scope.currentUser = AuthService.currentUser();
});
});
In such cases I typically opt to use a service to coordinate things. Service's are instantiated using new and then cached, so you effectively get a singleton. You can then put in a simple sub/pub pattern and you're good to go. A basic skeleton is as follows
angular.module('some-module').service('myCoordinationService', function() {
var callbacks = [];
this.register = function(cb) {
callbacks.push(cb);
};
this.send(message) {
callbacks.forEach(function(cb) {
cb(message);
});
};
}).controller('controller1', ['myCoordinationService', function(myCoordinationService) {
myCoordinationService.register(function(message) {
console.log('I was called with ' + message);
});
}).controller('controller2', ['myCoordinationService', function(myCoordinationService) {
myCoordinationService.send(123);
});
Do you use any serivce to keep logged user data? Basically serivces are singletons so they are good for solving that kind of problem without polluting $rootScope.
app.controller('LoginController', ['authService', '$scope', function (authService, $scope) {
$scope.login = function(username, password) {
//Some validation
authService.login(username, password);
}
}]);
app.controller('HeaderController', ['authService', '$scope', function (authService, $scope) {
$scope.authService = authService;
}]);
In your header html file:
<span ng-if="authService.isAuthenticated()">
{{ authService.getCurrentUser().userName }}
</span>
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.
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() {});