I'm struggling to get Angular route resolve working. I find the documentation less than useless for more complex parts of the javascript framework like this.
I have the following service:
app.service("AuthService", ["$http", "$q", function($http, $q){
this.test = function(){
return $q(function(resolve, reject){
var auth = false;
resolve(auth);
});
}
}]);
Now my routes looks like this:
app.config(["$routeProvider", "$locationProvider", function($routeProvider, $locationProvider){
$locationProvider.html5Mode(true).hashPrefix("!");
$routeProvider
.when("/account", {
templateUrl: "/views/auth/account.html",
controller: "AccountController",
resolve: {
auth: ["AuthService", function(AuthService) {
return AuthService.test().then(function(auth){
if (auth) return true;
else return false;
});
}]
}
});
}]);
What I want to happen here is the following:
User goes to /account
AuthService is fired and returns a variable (true or false)
In the resolve, if the returned value is false, the route cannot be loaded
If the returned value is true, the user is authenticated and can view the route
I don't think I've fully understood how to use resolve and my method so far does not work. Could someone please explain the correct way to do this?
The resolve block when configuring routes is designed to make navigation conditional based upon the resolution or rejection of a promise.
You are attempting to handle this by resolving with a resolution value of true or false
Please try the following:
resolve: {
auth: ["AuthService", function(AuthService) {
return AuthService.test().then(function(auth){
if (!auth){
throw 'not authorized';
}
});
}]
}
This will cause the promise to be rejected and therefore not allow the routing to continue/complete.
Also of note is that the value coming out of the promise resolution will be injected into the handling controller
This solution works for me, I also tried with .then function but it's incorrect because resolve performs it.
resolve: {
auth:
["AuthService", "AnotherService", "$rootScope", function(AuthService, AnotherService, $rootScope) {
if ($rootScope.yourCondition){
return AuthService.getFunction();
}
else{
return AnotherService.getAnotherFunction();
}
}]
},
views: {
'partialView#': {
/* == Component version == */
component: "yourComponent",
bindings: {
auth: 'auth', // Inject auth loaded by resolve into component
params: '$stateParams'
}
}
}
Related
I've been doing some Googling around this already but I'm unable to find a solution that works.
I'm using AngularJS 1.5.5 and .NET Web API 2 to build a web application and I would quite simply like to hide the ng-view element until all resolves have completed on the route.
I'm trying to use the $routeChangeStart and $routeChangeSuccess to set a variable on the $rootScope that is used in the index html to display the loading indicator and hide the content until the variable is false.
Here is my routing code for the routeChange properties:
_app.config([
'$routeProvider', '$httpProvider', '$provide',
function ($routeProvider, $httpProvider, $provide) {
$routeProvider.when('/Account',
{
templateUrl: '/Content/js/areas/account/account.html',
controller: 'accountController',
resolve: {
$accountResolver: function (accountService) {
return accountService.getMyAccountData();
}
},
caseInsensitiveMatch: true
});
$routeProvider.otherwise({ redirectTo: '404' });
}
]);
_app.run(['$rootScope', '$location', '$window', '$q', 'authService',
function ($rootScope, $location, $window, $q, authService) {
$rootScope.$on("$routeChangeStart",
function (e, curr, prev) {
$rootScope.$loadingRoute = true;
});
$rootScope.$on("$routeChangeSuccess",
function (evt, next) {
$rootScope.$loadingRoute = false;
});
$rootScope.$on("$routeChangeError",
function (evt, next) {
$rootScope.$loadingRoute = false;
});
}]);
And here is my html using that $loadingRoute variable:
<body class="ng-cloak" data-ng-app="wishlist" data-ng-controller="appController">
<wl-header></wl-header>
<preloader ng-if="$loadingRoute"></preloader>
<section ng-view ng-if="!$loadingRoute" class="container ng-cloak"></section>
</body>
I understand that there's quite a lot of articles covering this but none seem to work in my case. $loadingRoute gets set to true when the route change starts, as expected, which I will see if I add {{$loadingRoute}} to the HTML before the <section></section> tag. However before the $accountResolveris resolved, the $routeChangeSuccess gets fired, setting $rootScope.$loadingRoute = false which is unexpected.
I was under the impression that $routeChangeSuccess only got fired after all resolves had completed on the current route.
Am I doing something really obviously wrong here? Or has Angular simply changed?
Edit: I would also like to add that this approach worked in previous projects, so I'm at a real loss as to what's going wrong. I could set $rootScope.$loadingRoute manually in each page controller but that feels too dirty and unmaintainable.
Edit 2:
_app.factory('accountService', [
'accountResource',
function (accountResource) {
var _self = this;
return {
register: function (authData) {
return accountResource.register(authData);
},
getMyAccountData: function () {
return accountResource.getMyAccountData();
}
}
}
]);
_app.factory('accountResource', [
'$resource', 'rootUrl',
function ($resource, rootUrl) {
var api = rootUrl() + 'api/Account';
return $resource(api,
{},
{
register: {
method: 'POST',
url: '{0}/register'.format(api)
},
getMyAccountData: {
method: 'GET',
url: '{0}/GetMyAccountData'.format(api)
}
});
}
])
In order for a resolver to delay route change, it should return a promise. Otherwise route change happens immediately, this is what happens when $routeChangeSuccess is triggered before a promise from accountService.getMyAccountData() is resolved.
The problem is $resource methods (and so accountService.getMyAccountData()) return self-filling object that is populated with data asynchronously. A promise for this data is available as $promise property (see the reference), so it should be used for a resolver:
$accountResolver: function (accountService) {
return accountService.getMyAccountData().$promise;
}
If accountService is supposed to be purely promise-based wrapper for accountResource, a cleaner way to do this is to return a promise from its methods instead:
getMyAccountData: function () {
return accountResource.getMyAccountData().$promise;
}
This resolve function works everywhere else, and I'm stumped as to why the profile page is still executing when the promise is rejected. We produced this bug by logging in, deleting the token from storage and then trying to navigate to the profile page. It goes through all the authentication steps, and logs out. It hits the redirection line, but then it still loads the profile page, throwing errors because the auth has been cleared.
Any help you can offer would be great. Let me know if you need more info.
app.config('$routeProvider')
.when('/profile', {
templateUrl: '/templates/profile.html',
controller: 'ProfileController',
resolve: {
auth: function (SessionService) {
SessionService.resolve()
}
}
}).
otherwise({
redirectTo: '/login'
})
function resolve () {
if (self.isAuthenticated()) {
return $q.when({auth: true})
} else {
$q.reject({auth: false})
self.logout() // first we go here
}
}
}
function logout () {
var auth = self.storage()
if (...) {
...
} else {
self.clearUserAuthentication() // then here
$location.path('/login') // it redirects here, but still initializes the profile controller
}
}
/profile route always resolves because auth always returns resolved promise (or better put it: it doesn't return rejected promise and doesn't throw exception). Correct code would be:
.when('/profile', {
templateUrl: '/templates/profile.html',
controller: 'ProfileController',
resolve: {
auth: function (SessionService) {
return SessionService.resolve()
}
}
}).
Note, that it's very important that auth handler returns promise. If you omit return keyword it results into implicit return undefined. This is meaningless but is still considered as resolved promise.
In my scenario, when a visitor navigates to a page (or route) of the first time they should be anonymously authenticated (I'm using Firebase). For context; later on the visitor may migrate their anonymous session after they have logged in, with Facebook, for example.
If the anonymous authentication fails for some reason, they are redirected to an error page (route) -- one of few pages that do not require any authentication.
I am using promises to:
check if the visitor is already authenticated
if they are not, try and anonymously authenticate them
if they are authenticated successfully, resolve the promise (and route)
if the authentication fails for some reason, reject the promise
Question:
The promise always seems to be rejected the first time a visitor navigates to a page (route) that requires authentication, even when the visitor has been anonymously authenticated successfully (in step 2 above); why would this be happening?
Below I have included my code, and added a comment to highlight the section that seems to be causing the problem.
Thanks for your help with this, it is always appreciated!
var app = angular.module('vo2App', ['firebase', 'ngCookies', 'ngRoute']);
app.config(['$locationProvider', '$routeProvider', function ($locationProvider, $routeProvider) {
$routeProvider
.when('/', {
controller: 'HomeCtrl',
templateUrl: '/views/home.html'
})
.when('/login', {
controller: 'LoginCtrl',
templateUrl: '/views/login.html'
})
.when('/oops', {
controller: 'OopsCtrl',
resolve: {
currentAuth: function (){
return null;
}
},
templateUrl: '/views/oops.html'
});
}]);
app.run(['$location', '$rootScope', 'Auth', 'ErrorMsg', function ($location, $rootScope, Auth, ErrorMsg) {
$rootScope.$on('$routeChangeError', function (event, next, prev, error) {
if (error === 'AUTH_REQUIRED') {
ErrorMsg.add('Unauthorized');
// TODO: Make all error messages constants
$location.url('/oops');
}
});
$rootScope.$on('$routeChangeStart', function (event, next, current) {
if (! ('resolve' in next)) {
next.resolve = {};
}
if (! ('currentAuth' in next.resolve)) {
next.resolve.currentAuth = function ($q, Auth) {
var deferred = $q.defer();
Auth.$requireAuth().then(deferred.resolve, function () {
/* ** The following line seems to be causing the problem ** */
Auth.$authAnonymously().then(deferred.resolve, deferred.reject('AUTH_REQUIRED'));
});
return deferred.promise;
};
}
});
}]);
deferred.reject is not passed to then as error function, it is called every time.
Auth.$authAnonymously().then(deferred.resolve, deferred.reject('AUTH_REQUIRED'));
That's why deferred.promise is always rejected.
And you should possibly know that the code above is usually referred to as deferred antipattern. It is preferable to use existing promises instead of creating a new one with defer().
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;
}
},
When using the resolves in ui-router, is it possible to have something resolve first (an authentication check for example) before the other resolves on a state are even started?
This is an example of how I'm doing my authentication check right now:
angular.module('Example', [
'ui.router',
'services.auth',
'services.api'
])
.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('order', {
url: '/order',
views: {
'main': {
templateUrl: 'order/order.tpl.html',
controller: 'OrderCtrl'
}
},
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', function (auth) {
return auth.loggedIn();
}],
cards: ['api', function (api) {
return api.getCards();
}],
shippingAddresses: ['api', function (api) {
return api.getShippingAddresses();
}]
}
});
}]);
I'd like loggedIn to resolve first before cards and shippingAddresses start.
I could just put the other API calls in the controller (that's where they were before), but I'd like the data retrieved from them to be available before the templates start rendering.
This might work but then I couldn't inject the resolved values of each individual API call.
...
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', 'api', function (auth, api) {
return auth.loggedIn()
.then(api.getCards())
.then(api.getShippingAddresses())
.then(function () {
// whatever....
});
}]
}
...
Edit:
Looks like the answer here solves my issue, maybe it's not the best way to solve this problem but it works.
Just by injecting the resolved value of the authentication check it will wait until it is resolved before continuing on with the other resolves.
...
resolve: {
// I want this one to resolve before any of the others even start.
loggedIn: ['auth', function (auth) {
return auth.loggedIn();
}],
cards: ['api', 'loggedIn', function (api, loggedIn) {
return api.getCards();
}],
shippingAddresses: ['api', 'loggedIn', function (api, loggedIn) {
return api.getShippingAddresses();
}]
}
...
I would answer this myself but I'm going to leave it open for other people to provide their solutions.
I've provided some proof of concept how promises can be combined together: http://jsfiddle.net/pP7Uh/1/
Any rejects will prevents from calling next then method
resolve: {
data: function (api) {
var state;
return api.loggedIn().then(function(loggedIn) {
state = loggedIn
return api.getCards();
}).then(function(getCards) {
return {
loggedIn: state,
getCards: getCards
}
}).catch(function(error) {
console.error('error', error)
});
}
}
After this in data resolved object you will have:
{
loggedIn:"calling loggedIn",
getCards:"calling getCards"
}