According to documentation, you can use a custom data and listen for stateChanges to prevent accessing the state based on some condition. This works without a trouble but my use case is, when I'm entering a nested state I want all of the descendant states rules be enforced.
In other word, if I'm starting from green state and I want to end up being in blue state, I want the login rule gets evaluated. Currently, only color rule is applied. Is there any way to do this?
I understand that the data is overwritten on child state, and that's the reason that I only see the color log. But what is the way to extend data and not overwriting it?
here is the code:
var example = angular.module("example", ['ui.router']);
example.config(function ($stateProvider, $urlRouterProvider) {
$stateProvider
.state("parent", {
url: "/parent",
templateUrl: "templates/parent.html",
data: {
rule: function () {
console.log("login rule checked");
return true;
}
}
})
.state("parent.child", {
url: "/child",
templateUrl: "templates/child.html",
data: {
rule: function () {
console.log("color rule checked");
return true;
}
}
});
});
example.run(function ($rootScope, $state) {
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if (!toState.data || !angular.isFunction(toState.data.rule)) return;
var result = toState.data.rule();
if (result) {
event.preventDefault();
$state.go(toState, result.params, {notify: false});
} else {
event.preventDefault();
}
})
});
I'm not sure if this is exactly what you want, but doing this:
$state.go(toState, result.params, {notify: false, reload:true});
will ensure that the entire state is reloaded including all parent states. It's not exactly what you want since it may reload more than you want if state R is not the top of the tree. However, this should work in the example you are showing.
For more information, see the ui-router API.
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'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');
});
})
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.
I have the following question... or situation. I have states defined in my AngularJS app, like so...
$stateProvider
.state('myApp', {
abstract: true,
template: '<ui-view/>'
})
.state('myApp.stateOne', {
url: 'state1',
templateUrl: '/an/views/state-1.html',
controller: 'StateOneCtrl'
})
.state('myApp.stateTwo', {
url: 'state2',
templateUrl: '/an/views/state-2.html'
controller: 'StateTwoCtrl'
})
.state('myApp.stateThree', {
url: 'state3',
templateUrl: '/an/views/state-3.html'
controller: 'StateThreeCtrl'
})
There are more states and I have changed the naming for this example, but suppose I need to check if the user is allowed to see / load 'mayApp.stateThree'. I can determine this by asking the backend. I have a service (in this example called IsAllowedService) to deal with this requests / provide the access and normally I would write the logic to do the check in the .run() block in my app.js file for example:
.run(['IsAllowedService', '$state', function (IsAllowedService, $state) {
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState) {
// check if we are going to sfm.addContacts and if we are allowed to...
if (toState.name === 'myApp.stateThree') {
IsAllowedService.checkIfIsAllowed().then(function (resp) {
if(resp.allowed === false) {
$state.go('myApp.stateOne');
}
});
}
});
}]);
This works well but doesn't wait until we get the result from the service so 'mayApp.stateThree' is loaded then we a redirected if necessary. So we get a quick flash of the page before we are redirected. I could put the same code into the 'StateThreeCtrl' but I still get the flash / FOUC. Would it be possible to resolve this when defining the states, I know this won't work but something like this...
.state('myApp.stateThree', {
url: '/an/state3',
templateUrl: '/an/views/state-3.html'
controller: 'StateThreeCtrl',
resolve: {
isAllowed : function () {
IsAllowedService.checkIfIsAllowed().then(function (resp) {
return resp;
})
}
}
I realise that I wouldn't be able to inject the service (or even the $http service) but is it possible for me to somehow pause the loading of the view / controller of 'mayApp.stateThree' until I get the result from IsAllowedService.checkIfIsAllowed(). Any advice on how to structure my app / code would be appreciated. I have used ng-cloak in my HTML view but this did nothing!
Actually you're doing it almost right in the application's run block. Except you are not preventing anything. You can achieve that by adding:
event.preventDefault(); //Prevent from going to the page
Furthermore, you can add custom data to your $states , which will allow you to verify those conditions with your criteria. e.g.:
$stateProvider.state('home', {
controller: 'HomeController as home',
url: '/home',
templateUrl: 'home.html',
data: { roles: [ROLES.ANONYMOUS] }}); //This can be any condition
$stateProvider.state('user', {
controller: 'UserController as user',
url: '/user',
templateUrl: 'user.html',
data: { roles: [ROLES.ADMIN, ROLES.USER] }});
You can retrieve this custom data in the $stateChangeStart event:
$rootScope.$on('$stateChangeStart', function (event, next) {
if (!yourService.isAuthorized(next.data.roles)) {
event.preventDefault(); //Prevent from going to the page -> no flickering
$state.go('403'); //Or whatever is desired.
}
});
You see the flickering because you're using a Promise and the first page only gets redirected when the promise is furfilled. You can stop the flickering by preventing the default action, authorize and continue your flow as you desire when the promise resolves.
if (toState.name === 'myApp.stateThree') {
event.preventDefault(); //preventing the request.
IsAllowedService.checkIfIsAllowed().then(function (resp) {
if(resp.allowed === false) {
$state.go('myApp.stateOne');
} else { //he actually is allowed to go to state three.
$state.go('myApp.stateThree');
}
}, function() { //in case the server has no answer
$state.go('myApp.stateOne'); //you probably want to prevent it too
} );
In my opinion, if these conditions do not change during runtime, i.e. user role based, you can retrieve them upon user verification so you don't need a promise to begin with. Hope this helps.
I made a similar post before and added a working plunker.
I'm using ui-router in my angular application. Currently I've two routes /signin & /user.
Initially it shows /signin when the user clicks on the login button, I'm sending a ajax request and getting the user id. I'm storing the user id in localstorage and changing the state to /user.
Now, what I want, if a user is not loggedin, and user changes the addressbar to /user, it'll not change the view, instead it'll change the addressbar url to /signin again.
I'm try to use resolve, but it's not working. My code is:-
module.exports = function($stateProvider, $injector) {
$stateProvider
.state('signin', {
url: '/signin',
template: require('../templates/signin.html'),
controller: 'LoginController'
})
.state('user', {
url: '/user/:id',
template: require('../templates/user.html'),
resolve:{
checkLogin: function(){
var $state = $injector.get('$state');
console.log("in resolve");
if (! window.localStorage.getItem('user-id')) {
console.log("in if")
$state.go('signin');
}
}
},
controller: 'UserController'
})
}
Please help me to solve this problem.
I don't think it's allowed to change states in the middle of a state transition.
So, the way to address it is to have the checkLogin resolve parameter (I changed it below to userId) to be a function that either returns a value or a promise (in this case, a rejected promise, if you can't get the user-id).
You'd then need to handle this in $rootScope.$on('$stateChangeError') and check the error code.
resolve: {
userId: function ($q, $window) {
var userId = $window.localStorage.getItem('user-id');
if (!userId) {
return $q.reject("signin")
}
return userId;
}
}
And redirect in the $stateChangeError handler:
$rootScope.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
if (error === "signin") {
$state.go("signin");
}
});
If someone has this problem, you can solve it, using timeout service. It will put state switching call at the end of queue.
Also, you should use promises. Rejecting it will prevent initialization of that state:
resolve:{
checkLogin: function(){
var deferred = $q.defer();
var $state = $injector.get('$state');
if (!window.localStorage.getItem('user-id')) {
$timeout(function(){$state.go('signin');});
deferred.reject();
} else {
deferred.resolve();
}
return deferred.promise;
}
},