AngularJS handle calling promise multiple times with some exceptions - javascript

I asked this question before (AngularJS handle calling promise multiple times) and now I have different obstacle. Now I have to get cities list but there is an exception.
Cities can be called multiple times like countries (in my old question) and I have to cache data to prevent multiple calls for same data(cities). Old question's solution can block multiple calls but now I have to let some calls (for new country's cities).
So my question is:
How can I cache cities data to prevent call for same data?
(My function have to catch if call is for new country's cities list or not. if yes: call service and get cities, if not: return cities from cache)
Here is my service:
var cityCache = {};
vm.getCities = function (countryCode) {
if (countryCode!=undefined && !cityCache[countryCode]) {
vm.cityPromise = $http({
method: 'POST',
cache: true,
url: API + '/api/Global/CountryCities',
data: {
"CountryCode": countryCode
}
}).then(function successCallback(response,countryCode) {
if (errorHandler(response.data)) {
console.log("cities come from ajax")
cityCache[response.config.data.CountryCode] = response.data;
console.log(cityCache)
return response.data
}
});
} else {
vm.cityPromise = $timeout(function () {//I use this to get promise object
return cityCache[countryCode]
}, 0)
console.log("cities comes from cache");
}
return vm.cityPromise;
}
Example:
Let's say I am calling getCities function 3 times in the same time. I am watching my network traffic via chrome. I see 3 ajax calls. It's normal. But sometimes, I call for same city. I need to edit my function that can understand if city data is already called before (kind of cache). For example: If i ask function 3 times with this arguments:
1-Give me the cities in Germany,
2-Give me the cities in Ireland,
3-Give me the cities in Germany (again),
It's calling 3 times. But I want 1 call for Germany, 1 call for Ireland. Just 2 calls.

Same answer as your other question, just map to country code to the promise.
Also same as before, consider the error case.
var vm = this;
vm.cityPromises = {};
function getCities(countryCode) {
if (!vm.cityPromises[countryCode]) {
vm.cityPromises[countryCode] = $http({
method: 'POST',
cache: true,
url: API + '/api/Global/Countries',
}).then(function successCallback(response) {
if (errorHandler(response.data)) {
console.log("ajax")
return response.data;
}
});
} else {
console.log("cache")
}
return vm.cityPromises[countryCode];
}

You can use your own promise here. Don't forget injecting the $q service.
var cityCache = {};
vm.getCities = function (countryCode) {
var deferred = $q.defer();
if (countryCode!=undefined && !cityCache[countryCode]) {
vm.cityPromise = $http({
method: 'POST',
cache: true,
url: API + '/api/Global/CountryCities',
data: {
"CountryCode": countryCode
}
}).then(function successCallback(response,countryCode) {
if (errorHandler(response.data)) {
cityCache[response.config.data.CountryCode] = response.data;
deferred.resolve(response.data);
}
else{
deferred.reject();
}
});
}
else {
vm.cityPromise = $timeout(function () {//I use this to get promise object
deferred.resolve(cityCache[countryCode]);
}, 0);
}
return deferred.promise;
}

Try to use the $q service from angular:
updated to prevent multiple call of same city:
FIDDLE
the service:
.service("cityService", function($http, $q, $httpParamSerializerJQLike){
//var callCache = {};
var cityCache = {};
return {
getCities: function(countryCode){
//if(callCache[countryCode] === undefined){
var promise = $q.defer();
// callCache[countryCode] = promise;
//}else{
// console.log("return cached promise!!", callCache[countryCode]);
// return callCache[countryCode].promise;
//}
if (countryCode!=undefined && !cityCache[countryCode]) {
console.log("new city");
var data = $httpParamSerializerJQLike({
json: JSON.stringify({
name: countryCode+Math.random().toString(36).substring(7)
})
});
$http({
method: 'POST',
url:"/echo/json/",
data: data
}).then(function(risp) {
console.log("servicelog",risp.data);
cityCache[countryCode] = risp.data;
var obj = angular.extend({cache: false}, risp.data);
promise.resolve(obj);
//callCache[countryCode].resolve(obj);
//delete callCache[countryCode];
});
}else{
setTimeout(function(){
var obj = angular.extend({cache: true}, cityCache[countryCode]);
promise.resolve(obj);
//callCache[countryCode].resolve(obj)
//delete callCache[countryCode];
}, 1000)
}
return promise.promise;
}
}
});

I solved my problem by creating an object for the promise and many thanks to #Luke Harper for helping me before and now :) His answer is also correct but I must add a bit more code for my app.
If you see any problem in my code, please write to me so that I would edit the answer
So here is my solution:
vm.cityPromise = {};
vm.getCities = function (countryCode) {
vm.cityPromise["cityCache"] = countryCode;
if (!vm.cityPromise[countryCode]) {
if (countryCode != undefined && !cityCache[countryCode]) {
vm.cityPromise[countryCode] = $http({
method: 'POST',
cache: true,
url: API + '/api/Global/CountryCities',
data: {
"CountryCode": countryCode
}
}).then(function successCallback(response, countryCode) {
if (errorHandler(response.data)) {
cityCache[response.config.data.CountryCode] = response.data;
console.log("cities ajax, cityCache", cityCache)
return response.data
}
},function error (response){
console.log ("error:",response)
});
} else {
vm.cityPromise[countryCode] = $timeout(function () {
return cityCache[countryCode]
}, 0)
console.log("getCities cache");
}
}
return vm.cityPromise[countryCode];
}

Related

Using promises in javascript - only hitting the server once

I am trying to fully understand the usage of promises and the benefits they give. I have an AJAX call that grabs a bunch of data from the server. Right now I do not have promises implemented and the code hits the server anytime the user changes a view (all using the same data, just the way it looks).
Here is the promise I am trying to add:
function feedData(arr){
//data being initialized
this.initData();
}
feedData.prototype = {
constructor: feedData,
getData:function(){
return $.ajax({
url: 'php/getData.php',
dataType: 'json',
data: {
//data being sent over
}
});
},
initData:function(){
this.getData()
.done(function(result){
console.log(result.length);
})
.fail(function(x){
console.log(x);
});
},
....
}
I may not being fully understanding asyc behavior here. What I would have liked to do is get the result from getData and populate an object full of data that would be called whenever the user changes the view. From all I've read, thats not what promises are used for. Instead I should be returning a promise and using that data again? (Maybe this is my error of thought)
So my question is, once the data from getData is returned from AJAX, is there a way to return the promise and use the .done multiple times without hitting the server ever time? Meaning, since I will be using that same data and I can't save it to a global object, how could I achieve this?
Keep track of the promise returned by $.ajax(). This makes the call only once (in the constructor) regardless of how often you call getData():
function FeedData() {
this.data_promise = $.ajax({
url: 'php/getData.php',
dataType: 'json',
data: {}
});
}
FeedData.prototype = {
constructor: FeedData,
getData: function () {
return this.data_promise;
}
}
var feed = new FeedData();
feed.getData().then(function () {
/* .. */
});
You can also delay fetching until you call getData() for the first time:
function FeedData() {
this.data_promise = null;
}
FeedData.prototype = {
constructor: FeedData,
getData: function () {
if (this.data_promise === null) {
this.data_promise = $.ajax({
url: 'php/getData.php',
dataType: 'json',
data: {}
});
}
return this.data_promise;
}
}
Note, jQuery.ajax() returns a jQuery promise object.
At first successful $.ajax() call define a property to store the data at the instance. When .then() is called assign the result of $.ajax() to the value of the property at the object as a resolved Promise.
Retrieve the value from the object using instance.property.then().
function feedData(arr) {
var feed = this;
this.getData = function() {
return $.ajax({
url: 'php/getData.php',
dataType: 'json',
data: {
//data being sent over
},
// set `context` : `this` of `$.ajax()` to current `fedData` instance
context: feed
});
};
this.initData = function() {
// note `return`
return this.getData()
.then(function(result) {
console.log(result.length);
// define `this.promise` as a `Promise` having value `result`
this.promise = Promise.resolve(result);
return result;
})
.fail(function(x) {
console.log(x);
});
}
}
var request = new feedData();
request.initData().then(function(data) {
console.log(data)
});
// this will not make antoher request
request.promise.then(function(res) {
console.log("result:", res)
});
function feedData(arr) {
var feed = this;
this.getData = function() {
// do asynchronous stuff; e.g., `$.ajax()`
return $.Deferred(function(dfd) {
dfd.resolveWith(feed, [
[1, 2, 3]
])
});
};
this.initData = function() {
// note `return`
return this.getData()
.then(function(result) {
console.log(result.length);
// define `this.promise` as a `Promise` having value `result`
this.promise = Promise.resolve(result);
return result;
})
.fail(function(x) {
console.log(x);
});
}
}
var request = new feedData();
request.initData().then(function(data) {
console.log(data)
});
// this will not make another request
request.promise.then(function(res) {
console.log("result:", res)
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Angularjs factory calling a function: TypeError: function is not a function

I'm trying to call another function within my factory but i get this error. i did what others said in this link. i could not find the problem. where did i go wrong? Thanks!
.factory('UserPreferences',function(Http,ngDialog,Notification){
var settings = {}
return {
authenticated: false,
settings:{},
saveSettings: function(data){
var data = {
url: '/pointofsale/userpreferences/save',
data: data
}
Http.Post(data)
.success(function(data){
statusMessage = data
ngDialog.closeAll(0)
})
},
authenticate_change_settings: function(user,settings){
this.settings = settings
var data = {
url: '/pointofsale/authenticate_change_settings',
data: user
}
Http.Post(data)
.success(function(data){
if(data){
this.authenticated = true;
this.saveSettings(this.settings);
}
}).error(function(data){
Notification.error(data)
})
}
}
Update: so i found the problem, i can't call the function within the
$http post call success function. why is this happening?
So, i tried doing this. and it works, it called the function. not sure if this is proper.
authenticate_change_settings: function(user){
var $this = this;
var data = {
url: '/pointofsale/authenticate_change_settings',
data: user
}
Http.Post(data)
.success(function(data){
if(data){
$this.authenticated = true;
$this.saveSettings(this.settings);
}
}).error(function(data){
Notification.error(data)
})
}

$q promise with foreach

I am writing an angular service to work with SharePoint data and I have run into a problem. I have a function in my service that updates and single item and returns an $http promise which works fine. The problem is I am trying to write a function now that utilizes the first function to loop and update multiple items. I want it to return a single promise once all items have been updated and it should reject if any of the items being updated failed. Here is the function:
this.UpdateListItems = function (webUrl, listName, itemsJson) {
if (numItems == -1) {
numItems = itemsJson.length;
c = 0;
f = 0;
}
var promises = [];
itemsJson.forEach(function (itemProps) {
var itemPromise = this.UpdateListItem(webUrl, listName, itemProps.Id, itemProps)
.then(function (response) {
c++;
if (c == numItems && f == 0) {
numItems = -1;
return itemsJson[listName];
}
}, function (error) {
c++; f++;
alert("ERROR!");//This gets called first alert below
if (c == numItems) {
numItems = -1;
return $q.reject(error);
}
});
promises.push(itemPromise.$promise)
}, this);
return $q.all(promises)
.then(function (data) {
alert("IN SUCCESS"); //This always gets called immediately after first item success instead of waiting for all items to finish
}, function (error) {
alert("IN ERROR"); //This never gets called
});
};
The $q.all is returning immediately after the first item returns successfully instead of waiting for the rest of the async item calls. Any help is much appreciated, I am new to all this. Thanks!
EDIT: Adding UpdateListItem code as requested:
this.UpdateListItem = function (webUrl, listName, itemId, itemProperties) {
if (typeof lists[listName] === 'undefined') {
lists[listName] = [];
}
var post = angular.copy(itemProperties);
DataUtilitySvc.ConvertDatesJson(post);
return this.GetListItemById(webUrl, listName, itemId)
.then(function (item) {
return $http({
url: item.__metadata.uri,
method: 'POST',
contentType: 'application/json',
processData: false,
headers: {
"Accept": "application/json;odata=verbose",
"X-HTTP-Method": "MERGE",
"If-Match": item.__metadata.etag
},
data: JSON.stringify(post),
dataType: "json",
}).then(function (response) {
var temp = [];
temp.push(itemProperties);
DataUtilitySvc.MergeByProperty(lists[listName], temp, 'Id');
return response;
}, function (error) {
return $q.reject(error);
});
}, function (error) {
return $q.reject(error);
});
};
Seems like this.UpdateListItem function already returned promise by having $promise object. That's why you were able to have .then(chain promise) function over it.
So basically you just need to push returned itemPromise object instead of having itemPromise.$promise inside promises array. Basically when you are doing $promise, it creates an array of [undefined, undefined, ...] and will resolve as soon as for loop completed.
Change to
promises.push(itemPromise)
from
promises.push(itemPromise.$promise)
Somewhat this question can relate to this answer

Using AngularJS service not getting the data

If i access the link http://localhost/cgi-bin/superCategory.pl?action=GET
I will get this data:
[{"name":"Baby Care","id":"2","image":"/images/categories/baby-care.png"},{"name":" Bread, Bakery & Dairy Products","id":"5","image":"/images/categories/dairy-products.png"},{"name":"Beverages","id":"6","image":"/images/categories/beverages.png"},{"name":"Others","id":"9","image":"/images/categories/others.png"}]
But when i try to get the same data using AngularJS service and controller, I m not getting the data. This is my controller and service code.
sampleApp.factory('SuperCategoryService', ['$http', function ($http){
var req = {
method: 'POST',
url: 'http://localhost/cgi-bin/superCategory.pl',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: { action: 'GET' }
};
return {
GetSuperCategories: function () {
return $http(req).then(
function(response)
{
if (typeof response === 'object')
{
return response;
}
else
{
alert ('wrong');
}
},
function(response) {
alert ('again worng');
// something went wrong
//return $q.reject(response.data);
});
}
};
}]);
sampleApp.controller('SuperCategoryController', function ($scope,SuperCategoryService) {
$scope.SuperCategories = [];
$scope.GetSuperCategories = function() {
SuperCategoryService.GetSuperCategories().then(
function(d) {
alert (d);
if (d !== undefined) {
alert ('in');
console.log(d);
$scope.SuperCategories = d;
}
else {
alert ('undefined data');
}
},
function(response) {
alert ('error worng');
// something went wrong
//return $q.reject(response.data);
});
};
$scope.GetSuperCategories();
});
though code is reaching to alert ('in'), but nothing is getting assigned to variable $scope.SuperCategories;
Can some one help me what i m doing wrong in assignment.
Your service function is doing $http using .then which is nothing but resolving using chain promise you should get explicit data from object you service will return response.data instead of response
GetSuperCategories: function () {
return $http(req).then(
function(response)
{
var data = response.data;
if (typeof data === 'object')
{
return data ;
}
else
{
alert ('wrong');
}
},
function(response) {
alert ('again worng');
// something went wrong
//return $q.reject(response.data);
});
}
};

How can I force this AngularJS service to wait until it has a value to return?

I have a service that is called by multiple controllers. It loads data into an object categories:
.service('DataService', ['$http', '$q', function($http, $q) {
var categories = {};
// Return public API.
return({
setCategory: setCategory,
getCategory: getCategory,
getJSON: getJSON,
categories: categories
});
function setCategory(name, category) {
console.log("setting category");
console.log(name, category)
categories[name] = category;
}
function getCategory(name) {
console.log("getCategories:");
console.log(categories[name]);
return categories[name];
}
function getJSON() {
//JSON stuff, where categories gets its initial values.
}
I call getCategory(name) in many places, and in some instances, it is called before categories has populated, e.g:
$scope.category = DataService.getCategory(name);
//$scope.category is undefined
How can I build this Service so that getCategories waits until categories is defined before returning its value? Alternately, how can I write the Controller so that getCategories isn't defined until categories has a value? I have tried using a $scope.$watch function in the controller to watch DataService.categories, to no success- it never logs an updated value.
Use the promises you're already injecting in your service. Here is just one of the many possible ways you can do this:
var pendingQueue = [];
var loaded = false;
var self = this;
function getCategory(name) {
var deferred = $q.defer();
if (loaded) {
// Resolve immediately
console.log('Already loaded categories, resolving immediately...');
deferred.resolve(self.categories[name]);
return deferred.promise;
}
// Queue the request
pendingQueue.push({
promise: deferred.promise,
name: name
});
if (pendingQueue.length === 1) {
console.log('First request for a category, requesting...
// We are the FIRST request. Call whatever it takes to load the data.
// In a 'real' language this wouldn't be thread-safe, but we only have one, so...
$http.get('/my-data').then(function(data) {
self.categories = data;
console.log('Loaded categories', self.categories);
loaded = true;
pendingQueue.map(function(entry) {
entry.promise.resolve(entry.name);
});
pendingQueue.length = 0;
});
}
return deferred.promise;
}
Then in your controller:
DataService.getCategory(name).then(function(category) {
// Do something with category here
});
This will:
For the first request, make the async request and then resolve the promise once the data is obtained.
For the second - Nth request BEFORE the data is obtained, queue those without making duplicate requests.
For requests AFTER the data is obtained, resolve immediately with the requested data.
No error handling is done - you should use deferred.reject() to send those back to the caller, and .catch() / .finally() to handle them in the controller(s).
There are many solutions - this is just one option.
Inside DataService
yourApp.service('DataService', function($resource, $q) {
var resource = $resource('/api/category/:id', {}, {
query: {
method: 'GET',
isArray: false,
cache: false
},
save: {
method: 'POST',
isArray: false,
cache: false
}
});
return {
getCategory: function(id) {
var deferred = $q.defer();
resource.query({id: id},
function(response) {
deferred.resolve(response);
},
function(response) {
deferred.reject(response);
}
);
return deferred.promise;
},
setCategory: function(categoryObj) {
var deferred = $q.defer();
resource.save(categoryObj,
function(response) {
deferred.resolve(response);
},
function(response) {
deferred.reject(response);
}
);
return deferred.promise;
},
getJSON: function() {
// stuff to do
}
};
});
Inside DataController:
yourApp.controller('DataCtrl', function($scope, DataService) {
$scope.handleSuccessResponse = function(response) {
$scope.data = response;
};
$scope.handleErrorResponse = function(response) {
$scope.error = response;
};
DataService.getCategory(123).then($scope.handleSuccessResponse, $scope.handleErrorResponse);
});

Categories