Im just learning ui-router resolve and would like to simply redirect my state if the user is not logged-in.
It seems I cannot simply use $state.go inside the callback function.
Here is my code:
.state('base.user', {
url: '/user',
templateUrl: 'views/user.html',
controller: 'userCtrl',
resolve: {
test: function($state, $q) {
var deferred = $q.defer();
if (!loggedIn()) { // resolves to false when not logged-in
deferred.reject();
$state.go('base.test'); // ** Throws "Possibly unhandled rejection" error
} else {
deferred.resolve();
/* and maybe do some more stuff.. */
}
}
}
})
I know this is often done with services and things like that, but for now, I would just like a simple working example.
The way we decided to handle it was to listen to $stateChangeError and if the error thrown contained a path, then go to that path.
$rootScope.$on('$stateChangeError', function(toState, toParams, fromState, fromParams, error) {
if (error.state) {
$state.go(error.state, error.stateParams);
}
});
And in you resolve:
test: function($q) {
var deferred = $q.defer();
if (!loggedIn()) {
deferred.reject({state: 'base.test'});
} else {
deferred.resolve();
}
return deferred.promise;
}
Implement the $stateChangeStart hook and check your redirection condition there
$rootScope.$on('$stateChangeStart', function (event, toState) {
if (toState.name === 'base.user') {
if(!loggedIn()) { // Check if user allowed to transition
event.preventDefault(); // Prevent migration to default state
$state.go('base.test');
}
}
});
Related
I'm trying to use meteor angular js ui-router resolve to load information of one user selected from user list.
$stateProvider
.state('userprofile', {
url: '/user/:userId',
cache: false,
template: '<user-profile userinfo="$resolve.userinfo"></user-profile>',
controller: UserProfile,
controllerAs: name,
resolve: {
userinfo: function($stateParams) {
viewedUser = Meteor.users.findOne({
_id: $stateParams.userId
});
return viewedUser;
},
}
});
The problem is that, for the first time after from user list, user profile display correctly. However, page reload makes the userinfo becomes undefined.
I guest that from second time, the controller loaded already so that it display before resolve done?!
After a while searching, I tried $q and $timeout
resolve: {
userinfo: function($stateParams, $q, $timeout) {
deferred = $q.defer();
$timeout(function() {
deferred.resolve(Meteor.users.findOne({
_id: $stateParams.userId
}));
}, 1000);
return deferred.promise;
},
}
It works as I expected, user profile displayed every time I refresh the page.
But if I lower the delay to 500, it back to undefined when refreshed.
I not sure why in this case, longer delay works?
Thank you!
Here is the code that I use,
resolve: {
currentUser: ($q) => {
var deferred = $q.defer();
Meteor.autorun(function () {
if (!Meteor.loggingIn()) {
if (Meteor.user() == null) {
deferred.reject('AUTH_REQUIRED');
} else {
deferred.resolve(Meteor.user());
}
}
});
return deferred.promise;
}
}
This is from a tutorial by #urigo somewhere, which took me some time to find, but it works like a charm.
This code is handy to trap the case where authentication is required - put it at the top level in a .run method
function run($rootScope, $state) {
'ngInject';
$rootScope.$on('$stateChangeError',
(event, toState, toParams, fromState, fromParams, error) => {
console.log("$stateChangeError: "+error);
if (error === 'AUTH_REQUIRED') {
$state.go('login');
}
}
);
}
You can try this routes in resolve
if you use angular-meteor
resolve: {
'loginRequired': function ($meteor, $state) {
return $meteor.requireUser().then(function (user) {
if (user._id) {return true;}
}).catch(function () {
$state.go('login');
return false;
});
}
}
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'm using event.preventDefault() technique to conditionally redirect user to an appropriate state based on a flag value which I'm fetching from backend. Eveything seems to be working fine except that sometimes the actual page is rendered and then redirection happens and that too in very few cases. This render of actual page is also for a very short duration. Following is the code snippet which I'm using:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
if (fromState.name === '' && toState.name === "myState"){
Auth.isLoggedInAsync(function(loggedIn) {
if (!loggedIn) {
event.preventDefault();
$state.go('state1');
} else {
event.preventDefault();
Auth.getNextStateForUser()
.then(function(data) {
console.log(data.nextState);
$state.go(data.nextState);
})
.catch(function(err){
console.log(err);
$state.go(toState.name);
});
}
});
The logic which I'm following is, check if a user is logged in, then redirect to state1, else redirect to next appropriate state based on nextstateValue fetched from backend. If some error occurs, open the actual page without any redirection.
I hope I make my myself clear with what I'm doing. Just want to know if the issue which I'm facing is a genuine issue or there is something wrong which I'm doing from my side.
I have posted it on github page as well but it seems that I'm the only one who is facing this issue.
Do check this for more info:
https://github.com/angular-ui/ui-router/issues/2088
Thanks
Edit: Based on the answers I have updated my code for using ui-router resolve and converted my isLoggedInAsyc to return promise.
resolve: {
promiseState: function(Auth, $state, $q, $stateParams) {
var deferred = $q.defer();
Auth.isLoggedInSync()
.then(function(isLoggedIn){
console.log(isLoggedIn);
if(isLoggedIn) {
Auth.getNextStateForUser()
.then(function(data) {
console.log(data.nextState);
$state.go(data.nextState);
deferred.resolve(data);
})
.catch(function(error) {
console.log(error);
deferred.reject(error);
});
}
deferred.resolve(isLoggedIn);
})
.catch(function(err){
console.log(err);
deferred.reject(err);
});
return deferred.promise;
}
}
And my isLoggedIn() returning promise.
isLoggedInSync: function(callback) {
var cb = callback || angular.noop;
var deferred = $q.defer();
if(currentUser.hasOwnProperty('$promise')) {
currentUser.$promise.then(function() {
deferred.resolve(true);
return cb(true);
}).catch(function() {
deferred.resolve(false);
return cb(false);
});
} else if(currentUser.hasOwnProperty('role')) {
deferred.resolve(true);
} else {
deferred.resolve(false);
}
return deferred.promise;
}
As mentioned by #milaenlempera, all Angular events are synchronous and there is no real way for you to nicely handle asynchrony in this instance. We had this issue in our production code and our solution was to use the resolve functionality in ui-router. This is how we solved it (plugged into your code obviously)
.state({
name: 'myState',
resolve: {
principal: ['Auth', '$state', '$q', function(Auth, $state, $q) {
var deferred = $q.defer();
Auth.isLoggedInAsync(function(loggedIn) {
if(!isLoggedIn) {
deferred.resolve($state.go('state1'));
return;
}
return Auth.getNextStateForUser()
.then(function(data) {
$state.go(data.nextState);
})
.catch(function(error) {
deferred.reject(error);
});
});
return deferred.promise;
}]
}
});
I'd recommend turning Auth.isLoggedInAsync to return a promise.
Also, your original code will cause an infinite loop if Auth.getNextStateForUser() is rejected. It will attempt to go to the state with the name toState.name, which is the state that just caused the error, which will cause another error and cause it to go to toState.name....
Because resolve cascades, you can implement child states if you want to share rules for permissions. It does really suck that there is no way to just put a property on the state and handle asynchronous permission checking elsewhere, but that's the hand we have been dealt at the moment.
As per comments, here's an example of promise chaining with your amended code.
promiseState: ['Auth', '$state', function(Auth, $state) {
return Auth.isLoggedInSync()
.then(function(isLoggedIn) {
if(isLoggedIn) {
return Auth.getNextStateForUser().then(function(data) {
return data.nextState;
});
}
// If the user isn't logged in, return the state that you should go to instead
return 'state1';
})
.then(function(nextState) {
return $state.go(nextState);
});
}]
Angular events are synchoronously.
If you need stop it, you must do it synchronously in event handler function.
In you example is asynchronously checked login, meanwhile routing continue. This is why you see page and to redirect happens afterwards.
$rootScope.$on('$stateChangeStart',
function(event, toState, toParams, fromState, fromParams){
// here check if route can be display (synchronously)
// or event.preventDefault()
Auth.isLoggedInAsync(function(loggedIn) {
if (loggedIn) {
// continue to original route or go to state by server data
} else {
// redirect to login page
}
});
you can be inspired in my solution here
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;
}
},
Inside my Angular application I handle routes/states via ui-router. If everything works - it is great. But what is a good way to handle errors occurring inside resolve functions?
My current solution:
I have a to dedicated error state (similar to the common 404.html). Which looks like this:
// inside config()
.state('error', {
url: '/error',
controller: 'ErrorCtrl',
templateUrl: 'error.html' // displays an error message
})
If an error occurs inside resolve I catch it via the broadcasted $stateChangeError in m run function:
angular.module('myModule').run(function($state) {
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
event.preventDefault();
$state.go('error');
});
});
This works, but I want to change my error message inside my 'error.html' dependent on the error. I don't want to pollute the $rootScope and I want to do it in a ui-router'esk way.
My current solution uses $stateParams to the error data to my error state, but I have to use JSONified query params for this and get a very ugly URL:
// inside config()
.state('error', {
url: '/error?data&status&config', // accept these three params
controller: 'ErrorCtrl',
templateUrl: 'error.html' // displays an error message
})
angular.module('myModule').run(function($state) {
$rootScope.$on('$stateChangeError', function(event, toState, toParams, fromState, fromParams, error) {
event.preventDefault();
$state.go('error', JSON.stringify(error)); // error has data, status and config properties
});
});
Question
Is there a different way how I can pass the error object to my error state without uglifying my URL? (Note: My error object is complex and not just a simple string.)
A strategy that worked for me is to have the resolve functions return a rejected promise with the error object:
$stateProvider.state('needs_authentication', {
url: '/auth',
resolve: {
user: function ($q, authService) {
if (authService.isAuthenticated()) {
...
}
else {
var errorObject = { code: 'NOT_AUTHENTICATED' };
return $q.reject(errorObject);
}
}
}
});
This lets your $stateChangeError function be able to handle specific error conditions:
$rootScope.$on('$stateChangeError', function (evt, toState, toParams, fromState, fromParams, error) {
if (angular.isObject(error) && angular.isString(error.code)) {
switch (error.code) {
case 'NOT_AUTHENTICATED':
// go to the login page
$state.go('login');
break;
default:
// set the error object on the error state and go there
$state.get('error').error = error;
$state.go('error');
}
}
else {
// unexpected error
$state.go('error');
}
});
You could pass data through a service, that you should access in your error controller or in onEnter method of error state.
Or you could "enrich" your error state. Maybe it's NOT the angular way, but what I mean is:
$rootScope.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
event.preventDefault();
$state.get('error').error = { code: 123, description: 'Exception stack trace' }
return $state.go('error');
});
And in your error state config:
.state('error', {
url: 'error',
resolve: {
errorObj: [function () {
return this.self.error;
}]
},
controller: 'ErrorCtrl',
templateUrl: 'error.html' // displays an error message
});
Hope this helps
You can remove the whole url: option and replace this with just a params: option.
.state('error', {
abstract: false,
templateUrl: templateFor('error'),
controller: controllerFor('error'),
params: { 'error': 'An error has occurred' },
resolve: {
error: [
'$stateParams', function ($stateParams) {
return $stateParams.error;
}
]
}
})
Now there is no ugly url. and the controller can get the error as a parameter.