I'm really struggling with the promises here.
It's more than a day now, and I still can't figure it out.
I'm new to Angular and, more in general, to the promises "concept", so I'm sure there is something I'm missing, but I can't figure it out.
Basically, I'm calling a remote web service using a $post request, and in the success method I update some data on $rootScope
LoginService
this.login = function(url, request) {
return $http.post( url, request ).
success(function(data) {
if (data.return_code === 0) {
userData = {
name: data.name,
role: data.role
}
/*
* Inside this function, $rootScope gets
* updated with userData
*/
storage.update('user_details', userData)
}
else {
// non authorized user
}
return userData
}).
error(function(data, status) {
throw new Error()
})
}
Then, in the controller, I do something like
$scope.login = function(url, request) {
loginService.login(url, request).then(function(response) {
/* this is a ui.router redirection to the state 'home' */
$state.go('home')
})
}
The problem is that in the new page (the home state), $rootScope is not updated, unless I do a page reload, which "solves" the issue.
It seems to me that the call of the promise does not wait for it's completion for the page redirect, but even wrapping $state.go in a $timeout doesn't solve the issue...
I'm really lost, any help would be appreciated
If storage update is some async action you want to wait for until it is complete and it returns a promise then you can simply chain it:
this.login = function(url, request) {
return $http.post(url, request).then(function(data){
if (data.return_code === 0) {
var userData = {
name: data.name,
role: data.role
};
return storage.update('user_details', userData).then(function(){
return userData;
});
}
else {
}
})
};
In usage:
loginService.login(...).then(function(userData){
//You reach here only after storage.update was completed
$state.go('home')
});
Related
My angular JS application is for an e-commerce usecase. There would be several pages, where some data would be fetched from some REST APIs which would be authenticated (and some not requiring authentication). If authentication fails (user not logged in), the APIs would all respond with a special error_code (say 'AUTH_FAIL').
My requirement is if any API fails due to authentication, then a login modal form dialog should appear in that page. This modal form contains the Username and password field. If the login succeeds, the modal window should close, and the current route should be re-freshed.
I understand how to do this for a particular route/controller. However, since there would be a lot of such pages where this would be needed, I'm unable to think of a way in which same piece of code could be easily utilized, since in my opinion, this does seem like a common requirement. How can it be done, or if not, what's the best way around it?
You can use interceptors for this purpose. Inteceptors can be used for global error handling, authentication, or any kind of synchronous or asynchronous pre-processing of request or postprocessing of responses.
For example I use the following code to redirect user to login when authentication fails.
.factory('myInterceptor', ['$q', '$location', '$injector', function ($q, $location, $injector) {
return {
response: function (response) {
return response || $q.when(response);
},
responseError: function (rejection) {
if (rejection.status === 401) {
var stateService = $injector.get('$state');
stateService.go('login');
}
return $q.reject(rejection);
}
}
}])
.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('myInterceptor');
}]);
Using interceptors sounds like the most obvious and elegant solution, however I was never satisfied with it, mostly because of running into the circular dependency problems.
Here are some bits and pieces of logic from one of my apps using angular 1.6 and ui-router.
Some explanation about the business logic before you deep dive into the code.
I use JWT authentication and my server expects JWT to be passed as a header, hence the specifics of the authService implementation. The authService checks if the header is expired, and tries to send a JWT refresh request before actually showing a login dialog. Feel free to adjust it to your security implementation (e.g. session cookie/based or some other storage).
authService.js
This service is responsible for storing security token in the client. It returns a promise, which is resolved with the JWT token (if present or if it was refreshed). The promise is rejected when the token is expired and the service failed to obtain new token from the server.
app.factory('authService', function($http, $q, $window, jwtHelper, API_HOST) {
var storage = $window.localStorage;
var cacheToken = {};
var targetUrl = null;
function saveToken(data) {
var tokenPayload = jwtHelper.decodeToken(data.auth_token);
storage.setItem('auth_token', data.auth_token);
storage.setItem('refresh_token', data.refresh_token);
storage.setItem('exp', tokenPayload.exp);
storage.setItem('user_identifier', tokenPayload.user_identifier);
cacheToken.auth_token = storage.getItem('auth_token');
cacheToken.refresh_token = storage.getItem('refresh_token');
cacheToken.exp = storage.getItem('exp');
cacheToken.user_identifier = storage.getItem('user_identifier');
}
function setCacheToken() {
cacheToken.auth_token = storage.getItem('auth_token');
cacheToken.refresh_token = storage.getItem('refresh_token');
cacheToken.exp = storage.getItem('exp');
cacheToken.user_identifier = storage.getItem('user_identifier');
}
function isAuthenticated() {
return cacheToken.auth_token && cacheToken.exp > moment(new Date().getTime()).unix()
}
setCacheToken();
return {
saveToken: function(data) {
saveToken(data);
return cacheToken;
},
getToken: function() {
return cacheToken;
},
isAuthenticated: isAuthenticated,
targetUrl: targetUrl,
getAuthorizationHeader: function() {
if (isAuthenticated()) {
return $q.when({
'Authorization': 'Bearer ' + cacheToken.auth_token
});
} else {
cacheToken.auth_token = storage.getItem('auth_token');
cacheToken.refresh_token = storage.getItem('refresh_token');
cacheToken.exp = storage.getItem('exp');
if (isAuthenticated()) {
return $q.when({
'Authorization': 'Bearer ' + cacheToken.auth_token
});
} else {
if (!cacheToken.refresh_token) return $q.reject(null);
return $http.post(API_HOST + '/tokens/refresh', {
'refresh_token': cacheToken.refresh_token
}).then(function(response) {
saveToken(response.data);
return {
'Authorization': 'Bearer ' + cacheToken.auth_token
};
}).catch(function() {
cacheToken = {};
$window.localStorage.clear();
return $q.reject(null);
})
}
}
}
}
});
app.run block
This piece of logic is responsible for memorising the target url in case user tried to access protected resource, or when user token/session is expired. Please 2 things here: authService.targetUrl stores the URL and authenticate property on the ui-router state is used to check if the state is protected (e.g. if the authentication logic should be applied).
$transitions.onBefore({
to: function(state) {
return state.self.authenticate;
}
}, function(trans) {
return authService.getAuthorizationHeader().then(function() {
return null;
}).catch(function() {
authService.targetUrl = $window.location.href;
$('#login-modal').modal();
return trans.router.stateService.target('homepage');
});
});
login modal directive
This piece of code stores the user token after login and also checks if the targetUrl is present in the authService, e.g. if a user tried to access protected resource some time before.
scope.loginCallback = function(response) {
authService.saveToken(response.data);
jasprApi.User.me().then(function(response) {
$rootScope.user = response.data;
$(element).modal('hide');
if (authService.targetUrl) {
$window.location.href = authService.targetUrl;
authService.targetUrl = null;
}
});
};
routes.js
Here is the ui-router states config which specified if the state should be protected
.state('admin', {
url: '/admin',
//other configuration
//...
//...
authenticate: true
})
api.js
A bonus — this is the sample from the file with the methods for accessing the API. Please note how authService is used here.
updatePageAction: function() {
return authService.getAuthorizationHeader().then(function(authHeader) {
return $http({
method: 'PUT',
url: '/admin/page/update',
headers: authHeader
});
});
},
I hope it helps!
Cheers
I am creating a resolve function, that validates a token in backend and returns success true or success false. IF false, it redirects to an error page, and if validated, it loads the page.
.when('/resetPassword/:id/:token', {
templateUrl: 'site/resetPassword/resetPassword.html',
controller: 'resetPasswordCtrl',
entitled: 'reset',
authenticate: true,
resolve: {
isValid: ['resetTokenService','$route' , function(resetTokenService, $route) {
return resetTokenService.validate($route.current.params.id, $route.current.params.token);
}]
}
})
This is the service:
app.service('resetTokenService', function (userResourceResourceFactory, $rootScope, $location, CONFIG, $http) {
var self = this;
var errorMsg;
$rootScope.errorObject = {errorMessage: null, errorCode: null,errorTime :null,errorStackTrace: null,url:window.location.href,partnerCode:null};
this.validate = function(id, token) {
$http({
method: 'POST',
url: CONFIG.MSA_URL + '/validate/' + id +'/' + token,
}).success(function (data) {
if (data.success) {
return true;
} else if (!data.success){
switch (data.error) {
case 'expired':
errorMsg = 'This link has expired. Please request a new one.';
break;
default : errorMsg = 'This link is invalid.';
}
$rootScope.errorObject.errorMessage=errorMsg;
$location.path('ErrorPage');
return false;
}
}).error(function () {
$rootScope.errorObject.errorMessage=errorMsg;
$location.path('ErrorPage');
return false;
});
};
});
It works well; however, if invalid, the password page flashes momentarily while it goes through the if and case statements. Therefore it is "resolved" when the backend call comes back, not when it returns true or false.
I am wondering how to make the page not flash and have the resolve {} function not do anything until it returns true or false / redirects
The validate function should be returning a promise. Currently, I believe your route is actually being resolved immediately (not when the backend call comes back), since your validate function doesn't return anything. This means that right now, it is probably always returning undefined to your controller.
You can start by returning the promise that comes from $http to see if things behave as you need:
this.validate = function(id, token) {
return $http({ ... });
}
However, this may not be enough because it looks like you need to redirect sometimes even if the promise from $http is a success. Whenever you want to prevent the state from loading, the promise you returned should be rejected.
So you may end up needing to return a promise that you manually create using the $q service, and then either resolving it (possibly with true, if you need the controller to see that boolean value), or rejecting it (in which case the controller will never load).
In the past, when using ui-router I've sometimes had to do hacky things like do the actual redirection inside a $timeout, or add an event handler to catch the routing error that happens when a promise is rejected, and do the redirection in there. I'm not sure if you'll have to do either of those in this case, but figured I'd mention it.
I have a "box" route/controller as below;
export default Ember.Controller.extend({
initialized: false,
type: 'P',
status: 'done',
layouts: null,
toggleFltr: null,
gridVals: Ember.computed.alias('model.gridParas'),
gridParas: Ember.computed('myServerPars', function() {
this.set('gridVals.serverParas', this.get('myServerPars'));
this.filterCols();
if (!this.get('initialized')) {
this.toggleProperty('initialized');
} else {
Ember.run.scheduleOnce('afterRender', this, this.refreshBox);
}
return this.get('gridVals');
}),
filterCols: function()
{
this.set('gridVals.layout', this.get('layouts')[this.get('type')]);
},
myServerPars: function() {
// Code to set serverParas
return serverParas;
}.property('type', 'status', 'toggleFltr'),
refreshBox: function(){
// Code to trigger refresh grid
}
});
My route looks like;
export default Ember.Route.extend({
selectedRows: '',
selectedCount: 0,
rawResponse: {},
model: function() {
var compObj = {};
compObj.gridParas = this.get('gridParas');
return compObj;
},
activate: function() {
var self = this;
self.layouts = {};
var someData = {attr1:"I"};
var promise = this.doPost(someData, '/myService1', false); // Sync request (Is there some way I can make this work using "async")
promise.then(function(response) {
// Code to use response & set self.layouts
self.controllerFor(self.routeName).set('layouts', self.layouts);
});
},
gridParas: function() {
var self = this;
var returnObj = {};
returnObj.url = '/myService2';
returnObj.beforeLoadComplete = function(records) {
// Code to use response & set records
return records;
};
return returnObj;
}.property(),
actions: {
}
});
My template looks like
{{my-grid params=this.gridParas elementId='myGrid'}}
My doPost method looks like below;
doPost: function(postData, requestUrl, isAsync){
requestUrl = this.getURL(requestUrl);
isAsync = (isAsync == undefined) ? true : isAsync;
var promise = new Ember.RSVP.Promise(function(resolve, reject) {
return $.ajax({
// settings
}).success(resolve).error(reject);
});
return promise;
}
Given the above setup, I wanted to understand the flow/sequence of execution (i.e. for the different hooks).
I was trying to debug and it kept hopping from one class to another.
Also, 2 specific questions;
I was expecting the "activate" hook to be fired initially, but found out that is not the case. It first executes the "gridParas" hook
i.e. before the "activate" hook. Is it because of "gridParas"
specified in the template ?
When I do this.doPost() for /myService1, it has to be a "sync" request, else the flow of execution changes and I get an error.
Actually I want the code inside filterCols() controller i.e.
this.set('gridVals.layout', this.get('layouts')[this.get('type')]) to
be executed only after the response has been received from
/myService1. However, as of now, I have to use a "sync" request to do
that, otherwise with "async", the execution moves to filterCols() and
since I do not have the response yet, it throws an error.
Just to add, I am using Ember v 2.0
activate() on the route is triggered after the beforeModel, model and afterModel hooks... because those 3 hooks are considered the "validation phase" (which determines if the route will resolve at all). To be clear, this route hook has nothing to do with using gridParas in your template... it has everything to do with callling get('gridParas') within your model hook.
It is not clear to me where doPost() is connected to the rest of your code... however because it is returning a promise object you can tack on a then() which will allow you to essentially wait for the promise response and then use it in the rest of your code.
Simple Example:
this.doPost().then((theResponse) => {
this.doSomethingWith(theResponse);
});
If you can simplify your question to be more clear and concise, i may be able to provide more info
Generally at this level you should explain what you want to archive, and not just ask how it works, because I think you fight a lot against the framework!
But I take this out of your comment.
First, you don't need your doPost method! jQuerys $.ajax returns a thenable, that can be resolved to a Promise with Ember.RSVP.resolve!
Next: If you want to fetch data before actually rendering anything you should do this in the model hook!
I'm not sure if you want to fetch /service1, and then with the response you build a request to /service2, or if you can fetch both services independently and then show your data (your grid?) with the data of both services. So here are both ways:
If you can fetch both services independently do this in your routes model hook:
return Ember.RSVP.hash({
service1: Ember.RSVP.resolve($.ajax(/*your request to /service1 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
service2: Ember.RSVP.resolve($.ajax(/*your request to /service2 with all data and params, may use query-params!*/).then(data => {
return data; // extract the data you need, may transform the response, etc.
},
});
If you need the response of /service1 to fetch /service2 just do this in your model hook:
return Ember.RSVP.resolve($.ajax(/*/service1*/)).then(service1 => {
return Ember.RSVP.resolve($.ajax(/*/service2*/)).then(service2 => {
return {
service1,
service2
}; // this object will then be available as `model` on your controller
});
});
If this does not help you (and I really think this should fix your problems) please describe your Problem.
I have an idea why it's not working, but not exactly how to fix this, and I've looked at the various similar questions/answers on SO but I can not fix my issue.
I am trying to call a service on page load to list a table of data, or let's just call this a list of urls.
The model would simply be { urls } would should return an array of urls, where in the future I'd use ng-repeat.
My controller:
function UrlListCtrl($scope, UrlService){
$scope.urls = UrlService.getUrls();
}
Service:
function UrlService($http) {
return {
getUrls: getUrls
}
function getUrls() {
console.log('Fetching Urls...');
return $http.get('urls/get')
.then(function(response) {
console.log('response:', response.data);
this.urlsResults = response.data;
});
}
}
The service does run, however it doesn't update the view. How can I fix this?
$http.get() is an async operation that returns a promise. You have to wait for this promise to be fulfilled, and then you can get the data from the response.
In your code $scope.urls = UrlService.getUrls(); was assigning the promise, and not it's results to $scope.urls, and the view was never updated when the operation ended.
Controller:
function UrlListCtrl($scope, UrlService){
UrlService.getUrls().then(function(response) {
$scope.urls = response.data;
});
}
Service:
function UrlService($http) {
return {
getUrls: getUrls
}
function getUrls() {
console.log('Fetching Urls...');
return $http.get('urls/get');
}
}
Let's say my current route is /books and I make an $http call to get all of the books we want to show a user. Normally, the call would resolve quickly and the books would be ng-repeated into the DOM. When we have an error, though (such as a timeout or there are no books returned), we update a common, global view that will overlay the content view and display a message like, "There are no books available." The common view is handled via a service with methods like CommonView.showLoading(), CommonView.showError("There are no books available."), and CommonView.hide(), etc.
Recently, I discovered that if the $http is not resolved quickly, the user may leave and go to another route (maybe /dinosaurs). Eventually, when the $http ends up resolving or being rejected, the promise call to display that common, global view will happen, resulting in an error view being displayed when there shouldn't be one, and the error will make no sense to the user (ie, user is at /dinosaurs and the error screen pops up with "There are no books available.").
I've seen that you can cancel an $http with a timeout promise, but this still seems like it could lead to race conditions (maybe you call cancel after processing of the resolve() or reject() has begun). I think it would be messy to have to check that the current route matches the route the $http was initiated from.
It seems like there should be some standard way to destroy $http calls on a route change or from a controller's $destroy method. I'd really like to avoid adding a lot of conditionals all over my gigantic app.
I can't find a great way to stop the processing of my callback if it's already started, but here's the $http wrapper I made to try and stop delayed callbacks from getting called after route changes. It doesn't replicate all of the $http methods, just the ones I needed. I haven't fully tested it, either. I've only verified that it will work in normal conditions (normal bandwidth with standard calls, ie httpWrapper.get(url).success(cb).error(err)). Your mileage may vary.
angular.module('httpWrapper', []).provider('httpWrapper', function() {
this.$get = ['$rootScope','$http','$q', function($rootScope, $http, $q) {
var $httpWrapper = function(config) {
var deferred = $q.defer();
var hasChangedRoute = false;
var canceler = $q.defer();
var http = null;
var evListener = null;
var promise = deferred.promise;
if ((config || {}).timeout && typeof config.timeout === 'Object') {
// timeout promise already exists
canceler.promise = config.timeout;
} else {
angular.extend(config || {}, {
timeout: canceler.promise
});
}
http = $http(config)
.success(function(data, status, headers, config) {
// only call back if we haven't changed routes
if (!hasChangedRoute) {
deferred.resolve({data:data, status:status, headers:headers, config:config});
}
})
.error(function(data, status, headers, config) {
// only call back if we haven't changed routes
if (!hasChangedRoute) {
deferred.reject({data:data, status:status, headers:headers, config:config});
}
});
evListener = $rootScope.$on('$locationChangeStart', function(scope, next, current) {
hasChangedRoute = true;
canceler.resolve('killing http');
evListener(); // should unregister listener
})
promise.success = function(fn) {
promise.then(function(response) {
fn(response.data, response.status, response.headers, config);
});
return promise;
};
promise.error = function(fn) {
promise.then(null, function(response) {
fn(response.data, response.status, response.headers, config);
});
return promise;
}
return promise;
};
angular.forEach(['get', 'delete', 'head', 'jsonp'], function(method) {
$httpWrapper[method] = function(url, config) {
return $httpWrapper(
angular.extend(config || {}, {
method: method,
url: url
})
);
};
});
angular.forEach(['post', 'put'], function(method) {
$httpWrapper[method] = function(url, data, config) {
return $httpWrapper(
angular.extend(config || {}, {
method: method,
url: url,
data: data
})
);
};
});
return $httpWrapper;
}];
});