AngularJS: Avoid calling same REST service twice before response is received - javascript

I have two directives, each consuming the same factory wrapping a $q/$http call.
angular.module("demo").directive("itemA", ["restService", function(restService) {
return {
restrict: "A",
link: function(scope, element, attrs) {
restService.get().then(function(response) {
// whatever
}, function(response) {
// whatever
});
}
};
}]);
angular.module("demo").directive("itemB", ["restService", function(restService) {
return {
restrict: "A",
link: function(scope, element, attrs) {
restService.get().then(function(response) {
// whatever
}, function(response) {
// whatever
});
}
};
}]);
angular.module("demo").factory("restService", ["$http", "$q", function($http, $q) {
return {
get: function() {
var dfd = $q.defer();
$http.get("whatever.json", {
cache: true
}).success(function(response) {
// do some stuff here
dfd.resolve(response);
}).error(function(response) {
// do some stuff here
dfd.reject(response);
});
}
};
}]);
Problem: When I do this
<div item-a></div>
<div item-b></div>
I get the same web service fired off twice, because the GET from ItemA is still in progress when the GET for ItemB goes.
Is there a way for whichever fires second to know that there's already a request to this in progress, so that it can wait a minute and grab it for free?
I've thought about making an $http or $q wrapper which flags each URL as pending or not but I'm not sure that's the best way. What would I do if it was pending? Just return the existing promise and it'll resolve when the other resolves?

Yes, all you need to do is to cache the promise and clean it off after the request is done. Any subsequent request in between can just use the same promise.
angular.module("demo").factory("restService", ["$http", "$q", function($http, $q) {
var _cache;
return {
get: function() {
//If a call is already going on just return the same promise, else make the call and set the promise to _cache
return _cache || _cache = $http.get("whatever.json", {
cache: true
}).then(function(response) {
// do some stuff here
return response.data;
}).catch(function(response) {
return $q.reject(response.data);
}).finally(function(){
_cache = null; //Just remove it here
});
}
};
}]);

Related

How to do multiple validations based on one http call in Angular

I have a field on a form with lots of validation.
At first, I had it structured into multiple directives, each with its own error message.
However, the validation uses a back-end asynchronous call, so suddenly for one field I was making 5 http calls for the same dataservice. I am trying to figure out how to write this more efficiently.
I was wondering if it is possible to have one $async validator that calls the dataservice, and multiple regular $validators inside of the first asynchronous function after .then. I experimented with this but it doesn't seem to reach the nested $validators at all.
I also tried to do the call once in a service, but I don't know how to get it to update when the modelValue on the field changes, and consequently pass the information to the respective validation directives. Could I do this as async validation in a service and attach the response to scope for the directives to look for?
TLDR;
How can I make ONE http call and based off of the returned data, perform multiple validation checks, each with its own error?
FOR EXAMPLE
I have about four directives that all look like this:
angular.module('validationForField').directive('po', ['$q', '$sce', '$timeout', 'myService', function ($q, $sce, $timeout, myService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl, ngModel) {
ctrl.$asyncValidators.validateField = function (modelValue) {
var def = $q.defer();
myService.httpcall(modelValue)
.then(function (response, modelValue) {
if (response.data.status === "Error") {
return def.reject();
}
def.resolve();
}).catch(function(){
def.reject();
});
return def.promise;
}
}
}
}]);
Each one has different analysis of the data to return different error messages. Each one makes a call to myService.httpcall which ends up being redundant because they are all getting the same data.
I am trying to do
angular.module('validationForField').directive('po', ['$q', '$sce', '$timeout', 'myService', function ($q, $sce, $timeout, myService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl, ngModel) {
ctrl.$asyncValidators.validateField = function (modelValue) {
var def = $q.defer();
myService.httpcall(modelValue)
.then(function (response, modelValue) {
if (response.data.status === "Error") {
return def.reject();
}
ctrl.$validators.checkStatus = function (response) {
if (response.data.data.status === "10"){
return false
}
ctrl.$validators.checkPermissions = function (response) {
return response.data.data.permission){
}
def.resolve();
}).catch(function(){
def.reject();
});
return def.promise;
}
}
}
}]);
This way there is the main async validator as to whether the http call is successful or not, and internal $validators that use that data when it returns
I assume the backend service accepts a value (the value of the field to be validated) and returns a single response for all validations, e.g.:
// true would mean valid, string would mean invalid with the given error:
{
businessRuleOne: true,
businessRuleTwo: "The format is incorrect",
...
}
I believe the solution is executing the HTTP call in a service that caches the promise; the async validators call the service and retrieve the same promise, which they return. Some sample code with inline explanation:
// the service:
app.service('myService', function($http, $q) {
// cache the requests, keyed by the model value
var requestMap = {};
this.httpcall = function(modelValue) {
// if cached, return that (and do not make extra call)
if( requestMap[modelValue] ) {
return requestMap[modelValue];
}
// if not cahced, make the call...
var promise = $http.get('....');
// ...cache it...
requestMap[modelValue] = promise;
// ...and remember to remove it from cache when done
promise.finally(function() {
delete requestMap[modelValue];
});
return promise;
};
});
Now the async validators can be implemented exactly as you post. Calling myService.httpcall(modelValue) will invoke the remote service only for the first call, the rest will reuse the cached promise.
Two more points: (1) This technique is called memoization. It is implemented by many libraries, e.g. lodash, you may be able to use those to keep myservice.httpcall() clean. (2) You do not need an extra promise from the async validators, e.g.:
angular.module('validationForField').directive('po', ['$q', '$sce', '$timeout', 'myService', function ($q, $sce, $timeout, myService) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, elem, attrs, ctrl, ngModel) {
ctrl.$asyncValidators.validateField = function (modelValue) {
return myService.httpcall(modelValue)
.then(function (response) {
if (response.data.status === "Error") {
return $q.reject();
}
return response;
});
}
}
}
}]);

AngularJS: Show loading spinner on ajax request only after some time has elapsed

In my Angular app, I have implemented this directive (code below) that basically allows me to show an element of my choosing whenever Angular detects an ajax request.
However, for slightly better usability, I would like to show the spinner only after some time has passed (say, 100 or 200 miliseconds) since the beginning of the request, to avoid those unnecessary split-second displays on every single request.
What would be the best way to implement such a thing? I'm having trouble getting setTimeout to play nicely within the if block because the element will never get hidden again, even if I no longer have a pending request.
.directive('loading', ['$http' ,function ($http)
{
return {
restrict: 'A',
link: function (scope, elm, attrs)
{
scope.isLoading = function () {
return $http.pendingRequests.length > 0;
};
scope.$watch(scope.isLoading, function (v)
{
if(v){
elm.show();
} else {
elm.hide();
}
});
}
};
}]);
Sounds like you can leverage interceptors and bind to a root variable instead of a directive to show your element for pending ajax requests (after the time threshold is met). Observe the following possibility...
app.factory('HttpInterceptor', ['$rootScope', '$q', '$timeout', function ($rootScope, $q, $timeout) {
return {
'request': function (config) {
$timeout(function() {
$rootScope.isLoading = true; // loading after 200ms
}, 200);
return config || $q.when(config);
},
'requestError': function (rejection) {
/*...*/
return $q.reject(rejection);
},
'response': function (response) {
$rootScope.isLoading = false; // done loading
return response || $q.when(response);
},
'responseError': function (rejection) {
/*...*/
return $q.reject(rejection);
}
};
}]);
// register interceptor
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push('HttpInterceptor');
/*...*/
}]);
<!-- plain element with binding -->
<div class="whatever" ng-show="isLoading"></div>
JSFiddle Link - working demo
For a single, globally-available loading indicator, an http interceptor is probably a better strategy. But assuming you want to attach this to individual elements separately, try something like this:
.directive('loading', ['$http', '$timeout', function($http, $timeout) {
return {
restrict: 'A',
link: function(scope, elm, attrs) {
scope.isLoading = function() {
return $http.pendingRequests.length > 0;
};
if (scope.isLoading) {
elm.hide(); // hide the loading indicator to begin with
// wait 300ms before setting the watcher:
$timeout(function() {
var watcher = scope.$watch(scope.isLoading, function(v) {
if (v) {
elm.show();
} else {
elm.hide();
watcher(); // don't forget to clear $watches when you don't need them anymore!
}
});
}, 300);
} else {
// No pending requests on link; hide the element and stop
elm.hide();
}
}
};
}]);
(You should probably also include a $destroy block on the directive to call watcher(), in case the directive goes out of scope while http requests are still pending.)

angular directive ng-repeat scope and Service

I am trying to create angular directive whom can run alone without a Controller, so I can set it anywhere I want, without adding the model to the controller.
The code is fairly simple:
App.directive('ngdPriceWithCurrencySelect',['CurrenciesService' ,function (CurrenciesService) {
return {
restrict: 'E',
replace: true,
scope: true,
link: function ($scope, $element, $attrs) {
$scope.currencies = CurrenciesService.getData();
$scope.$watch('currencies', function(newValue, oldValue){
console.log($scope.currencies);
});
},
template: '<select>\n\
<option ng-repeat="currency in currencies">{{currency.cur_name_he}}</option>\n\
</select>'
}
}]);
The Service actually return the data and the console.log($scope.currencies) shows object with the currencies.
but the repeater is not runing and I am not getting the result I want.
I thought this might be a scope problem, but I can't find a way to see the scope itsel. (angularjs batarang is not working in version 1.3+)
the problem can be in the Service as well so I am giving the service code:
App.service('CurrenciesService', ["$http", "$q", function ($http, $q) {
var service = {
returnedData: [],
dataLoaded: {},
getData: function (forceRefresh) {
var deferred = $q.defer();
if (!service.dataLoaded.genericData || forceRefresh){
$http.get("data/currencies").success(function (data) {
angular.copy(data, service.returnedData)
service.dataLoaded.genericData = true;
deferred.resolve(service.returnedData);
});
}
else{
deferred.resolve(service.returnedData);
}
return deferred.promise;
},
};
service.getData();
return service;
}]);
here is a JS fiddle for testing it: http://jsfiddle.net/60c0305v/2/
Your service returns a promise, not the data itself. You need to set a function for when the service resolves the promise:
link: function ($scope, $element, $attrs) {
// waiting for the service to resolve the promise by using the done method
CurrenciesService.getData().then(function(data) {
$scope.currencies = data;
});
$scope.$watch('currencies', function(newValue, oldValue){
console.log($scope.currencies);
});
}
Check this fiddle

First AngularJS resolve not working

The first time I visit a route with a resolve the request for the objects is not sent. The only way to visit the page is to make sure the route is correct in the url bar (typing or clicking a link) and refresh the page without caching (ctrl+shift+r in Firefox or ctrl+F5 in Chrome).
After I visit it the first time link will work.
app.config(['$stateProvider', function($stateProvider){
$stateProvider.state('users', {
templateUrl: '/app/Users/templates/users.html',
controller: 'Users',
resolve: {
'users': function(Objects, $stateParams){
return Objects.getUsers();
}
},
url: '^/users'
});
$stateProvider.state('user', {
templateUrl: '/app/Users/templates/user.html',
controller: 'User',
resolve: {
'user': function(Objects, $stateParams){
return Objects.getUser($stateParams.id);
}
},
url: '^/users/:id/'
});
}]);
app.factory('Objects', ['$http', '$q', function($http, $q){
/* Retrieve objects once */
var _cache = {};
function cache(key, promiseGetterFn) {
if (key in _cache) {
return _cache[key];
}
else {
var promise = promiseGetterFn();
_cache[key] = promise;
return promise;
}
}
return {
unsetKey: function(key){
delete _cache[key];
},
getUsers: function() {
return cache('users', function () {
var deferred = $q.defer();
$http.get(HOST + '/api/v1.0/users/all').then(
function (result) {
deferred.resolve(result);
});
return deferred.promise;
});
},
/*
getUsers: function(){
return cache('users', function(){
return $http.get(HOST + '/api/v1.0/users/all').success(
function(data, status, headers, config){
return data.users;
}
);
});
},
*/
/*
getUsers: function(){
return cache('users', function(){
var deferred = $q.defer();
return $http.get(HOST + '/api/v1.0/users/all').then(
function(result){
deferred.resolve(result.data.users);
},
function(status){
deferred.reject(status);
}
);
return deferred.promise;
});
},
*/
getUser: function(id){
return cache('user_' + id, function(){
var deferred = $q.defer();
return $http.get(HOST + '/api/v1.0/user/' + id).then(
function(result){
deferred.resolve(result.data.user);
},
function(status){
deferred.reject(status);
}
);
return deferred.promise;
});
},
};
}]);
app.run(['$rootScope', '$location', 'LocalService', function($rootScope, $location, LocalService){
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
if (!toState.publicAccess && !LocalService.get('loggedIn')){
/* Store the route they were trying to access */
LocalService.set('next', $location.path());
$location.path('/login');
}
});
}]);
Redirect after login code
app.factory('AuthInterceptor', ['$q', '$injector', '$location', 'LocalService', function($q, $injector, $location, LocalService){
/* Send Authorization in the header of each http request if there is a token */
return {
request: function(config){
if (LocalService.get('token')){
/* Using btoa to do Base64 */
/* LocalService.password is only used on login to get token and will be empty ('') when using the token */
config.headers.Authorization = 'Basic ' + btoa(LocalService.get('token') + ':' + LocalService.get('password'));
}
return config;
},
responseError: function(response){
if(response.status === 401 || response.status === 403){
/* Log the user out */
LocalService.unset('loggedIn');
LocalService.unset('token');
LocalService.unset('user');
$location.path('/login');
}
return $q.reject(response);
}
};
}]);
app.config(['$httpProvider', function($httpProvider){
$httpProvider.interceptors.push('AuthInterceptor');
}]);
app.run(['$rootScope', '$location', 'LocalService', function($rootScope, $location, LocalService){
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
if (!toState.publicAccess && !LocalService.get('loggedIn')){
/* Store the route they were trying to access */
LocalService.set('next', $location.path());
$location.path('/login');
}
});
}]);
app.controller('Login', ['$scope', '$http', '$location', 'growl', 'LocalService',
function($scope, $http, $location, growl, LocalService){
$scope.email = '';
$scope.password = '';
$scope.submitLogin = function submitLogin(){
LocalService.set('token', $scope.email);
LocalService.set('password', $scope.password);
$http.get(HOST + '/api/v1.0/token').
success(function(data, status, headers, config) {
LocalService.set('token', data.token);
LocalService.set('loggedIn', true);
LocalService.set('password', '');
/* Set current user */
$http.get(HOST + '/api/v1.0/authenticate').then(function(result) {
LocalService.set('user', JSON.stringify(result.data));
if (LocalService.get('next')){
var next = LocalService.get('next');
LocalService.unset('next');
console.log(next);
$location.path(next);
}
else{
$location.path('/');
}
});
}
).
error(function(data, status, headers, config) {
/* invalid credentials growl */
growl.addErrorMessage('Invalid username or password.');
}
);
};
}
]);
My first thought on this would be that the resolved objects are only resolved on hard load. Once the app has been instantiated from an index.html, the partial views may not be registering that the objects are promises. I would try making the returned objects from the factory (the api) actual promises. Now I have never seen a promise quite like yours, but I do not see any 'deferred.resolve' or a 'deffered.reject'. For instance:
return {
getBundles: function(){
return cache('bundles', function(){
var deffered = $q.defer
return $http.get(HOST + '/api/v1.0/bundles/all').success(
function(data, status, headers, config){
deferred.resolve(data.bundles);
}.error(function(status){
deferred.reject(status);
)};
return deferred.promise;
});
},
}
In doing this, I would also recommend that you return the objects to a javascript object before binding it to the view. This would be done in the controller with the 'bundles' injected into the controller.
var thisBundle = bundles.data.bundles;
thisBundle.then(function(data){
$scope.bundles = data;
});
Another solution I found was to wrap all of your items with a resolve in the routing. Try this:
resolve: {
'allProducts' : function(){
var theResolvePromise = $q.defer();
theResolvePromise.resolve({
bundles: ['Objects', function(Objects){
return Objects.getBundles();
}],
products: ['Objects', function(Objects){
return Objects.getProducts();
}],
technologies: ['Objects', function(Objects){
return Objects.getTechnologies();
}],
deliveryCategories: ['Objects', function(Objects){
return Objects.getDeliveryCategories();
}],
});
return theResolvePromise.promise;
};
}
}).
Then you would access this in the controller with the params passed through.
Retrieved from: http://www.undefinednull.com/2014/02/17/resolve-in-angularjs-routes-explained-as-story/
Hope this helps,
Patrick
Try this. I didn't test it, so it might not be perfect, but maybe it will help point you in the right direction.
app.factory('Objects', ['$http', function($http){
var _cache = {};
function cache(key, getterFn) {
if (_cache[key] == null) {
_cache[key] = getterFn();
_cache[key].then(function (result) {
_cache[key] = result;
});
}
return _cache[key];
}
return {
unsetKey: function(key){
delete _cache[key];
},
getUsers: function() {
return cache('users', function () {
return $http.get(HOST + '/api/v1.0/users/all');
});
},
getUser: function(id){
return cache('user_' + id, function() {
return $http.get(HOST + '/api/v1.0/user/' + id).then(function (result) {
return result.data.user;
});
});
},
};
}]);
$http.get returns a promise that resolves when the request completes. Returning a value from a .then() function will pop that value into the next .then() in the chain. UI-Router's resolves automatically unwrap the promise if given one. If given anything else, it just returns it. Promises don't automatically unwrap in Angular 1.2+ so you need to unwrap it yourself when you're working in your cache.
Your question is 2 part.
1) Why my first resolve not resolving the resolve for the state ?
The first time you return a "promise to fulfill a promise of fulfilling data". It is like a chained promise. It will take one extra $digest cycle to resolve this vs. if you returned a promise to get the data. $http.get() returns a promise.
In AngularJS the results of promise resolution are propagated asynchronously, inside a $digest cycle. So, callbacks registered with then() will only be called upon entering a $digest cycle.
Also check this out
Angular JS: Chaining promises and the digest cycle
2) What can you change to fix the problem ?
You can do 2 things
Just return the $http.get() because this is itself a promise.
Why cache a promise object ? You should cache the actual data.
Also with $resource and $http you can pass {cache: true} and the results will get cached. Alternatively if you like to take control you may want to use $cacheFactory. In this case you can place the actual results in the cache and not the promise object.

Angular- detecting a change in scope

I have a service that grabs JSON data for me and hands it off to a controller:
Service snippet:
...
getP2PKeywordData: function(global_m, global_y) {
// Return the promise
return $http({
url: base_url + 'get/P2P/kwords/',
method: "GET",
// Set the proper parameters
params: {
year: global_y,
month: global_m
}
})
.then(function(result) {
// Resolve the promise as the data
return result.data;
},
function(data) {
// Error handling
});
}
...
The controller successfully grabs the data, which I have tested with a console.log underneath the $scope.d3Data = data; line.
Controller snippet:
myApp.controller('DownloadsCloudCtrl', ['$scope',
'$rootScope',
'requestService',
'$cookieStore',
function($scope, $rootScope, requestService, $cookieStore) {
$rootScope.$on('updateDashboard', function(event, month, year) {
updateDashboard(month, year);
});
var updateDashboard = function(month, year) {
requestService.getP2PKeywordData(month, year).then(function(data) {
$scope.d3Data = data;
});
};
updateDashboard($cookieStore.get('month'), $cookieStore.get('year'));
}]);
The controller is hooked up to a d3-cloud directive (d3 word cloud) that actually appends the proper svg elements and draws the word cloud with the data. However, for some reason the controller above isn't passing the $scope.d3Data to the directive.
This is confusing because when I hardcode in an array of data into the controller, something like this...
$scope.d3Data = [
{
'kword': 'a',
'count': 20,
},{
'kword': 'b',
'count': 10,
...
... it connects to the directive perfectly!
Directive snippet:
myApp.directive('d3Cloud', ['$window',
'd3Service',
'd3Cloud',
function($window,
d3Service,
d3Cloud) {
return {
restrict: 'EA',
scope: {
data: '=',
label: '#'
},
link: function(scope, element, attrs) {
d3Service.d3().then(function(d3) {
window.onresize = function() {
scope.$apply();
};
scope.$watch(function() {
return angular.element($window)[0].innerWidth;
}, function() {
scope.render(scope.data);
});
scope.render = function(data) {
HTML snippet:
<div class="col-md-6">
<div class="module">
<div class="inner-module" ng-controller="DownloadsCloudCtrl">
<div class="module-graph">
<d3-cloud data="d3Data"></d3-cloud>
</div>
</div>
</div>
</div>
What have I tried:
I tried to add a manual $scope.$apply() after the $scope.d3Data = data; line in the controller. This, oddly, worked the first time I did it, but on every page refresh after that I got a "$digest already in progress" error (which was to be expected...).
In order to fix the $digest error, I tried encapsulating my $apply function in a $timeout code chunk, and even the dreaded $$phase conditional. Both of these solutions fixed the console error, but failed to solve the original problem of passing the data from the controller to the directive.
TL;DR: I'm fairly lost. Ideas on where to troubleshoot next?
It seems you are treating the response as a promise twice. So once in the service:
.then(function(result) {
// Resolve the promise as the data
return result.data;
},
And in the controller you resolve the promise again:
requestService.getP2PKeywordData(month, year).then(function(data) {
$scope.d3Data = data;
});
This can work because (from my understanding) Angular sometimes resolves promises automatically when binding to the scope.
It would be better to just handle the promise in the controller only. So the service becomes:
getP2PKeywordData: function(global_m, global_y) {
// Return the promise
return $http({
url: base_url + 'get/P2P/kwords/',
method: "GET",
// Set the proper parameters
params: {
year: global_y,
month: global_m
}
});
}
UPDATE:
Try to initialize the d3Data scope property to an empty collection, and then push the response data into it. For example:
myApp.controller('DownloadsCloudCtrl', ['$scope',
'$rootScope',
'requestService',
'$cookieStore',
function($scope, $rootScope, requestService, $cookieStore) {
//added
$scope.d3Data = [];
$rootScope.$on('updateDashboard', function(event, month, year) {
updateDashboard(month, year);
});
var updateDashboard = function(month, year) {
requestService.getP2PKeywordData(month, year).then(function(data) {
//then
angular.forEach(data, function(thing) {
$scope.d3Data.push(thing);
)};
});
};
updateDashboard($cookieStore.get('month'), $cookieStore.get('year'));
}]);

Categories