$q promise with foreach - javascript

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

Related

How can I serve a "resolve" in my recursive AJAX calls so that I can use 'done'?

I am doing a few recurring AJAX calls where I pass an array from the front-end to the back-end and whenever it comes back to the front-end, the array gets smaller (by 1) and ultimately it'll be empty, therefore my recursive calls will stop.
Here's my calls:
function download_required_files(demo_data) {
var ajaxsecurity = setup_page_params.ajax_nonce;
jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: ajaxsecurity,
content_install_request_data: JSON.stringify(demo_data),
},
success: function (response) {
console.log(response);
var data = response.data || false;
/**
* If no steps are left, meaning that all required files have been downloaded, proceed with the whole install process.
*/
if(!data.remaining_steps || !data.remaining_steps.length) {
return false;
}
if(data.can_continue !== 'yes') {
return false;
}
if(data.remaining_steps && data.remaining_steps.length) {
demo_data.steps_to_take = data.remaining_steps;
download_required_files(demo_data);
}
$('.demo-loader-content').fadeOut();
},
error: function (response) {
$('.demo-loader-content').fadeOut();
}
});
}
Assuming I have 2 steps to download files for, this download_required_files will run twice, then it'll be done, but if I do:
var download_process = download_required_files(demo_data) //Runs 2 times
download_process.done(function() { //Do stuff here once that function ran 2 times });
It gives me the: Cannot read property 'done' of undefined error and for good reason. That download_process is not a promise object for it to have that property, it's just...empty.
Where should I intervene in my download_required_files so that it signals to outside code that "Hey, in a promise environment, I'm done!"?
Although the result of the call to $.ajax is a jqXHR object, which is promise-like, for what you describe I think I'd go with your own native Promise (or Deferred if you prefer) to represent the overall recursive process:
function download_required_files(demo_data) {
return new Promise(function(resolve, reject) {
function worker() {
var ajaxsecurity = setup_page_params.ajax_nonce;
jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: ajaxsecurity,
content_install_request_data: JSON.stringify(demo_data),
},
success: function (response) {
console.log(response);
var data = response.data || false;
/**
* If no steps are left, meaning that all required files have been downloaded, proceed with the whole install process.
*/
if(!data.remaining_steps || !data.remaining_steps.length) {
// *** All done
$('.demo-loader-content').fadeOut();
resolve();
} else if(data.can_continue !== 'yes') {
// *** All done; but is this an error condition? If so
// use `reject` instead of `resolve` below.
$('.demo-loader-content').fadeOut();
resolve();
} else {
demo_data.steps_to_take = data.remaining_steps;
worker(); // This is the internal recursive call
}
},
error: function (response) {
$('.demo-loader-content').fadeOut();
}
});
}
worker();
});
}
Or using Deferred instead:
function download_required_files(demo_data) {
var d = $.Deferred();
function worker() {
var ajaxsecurity = setup_page_params.ajax_nonce;
jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: ajaxsecurity,
content_install_request_data: JSON.stringify(demo_data),
},
success: function (response) {
console.log(response);
var data = response.data || false;
/**
* If no steps are left, meaning that all required files have been downloaded, proceed with the whole install process.
*/
if(!data.remaining_steps || !data.remaining_steps.length) {
// *** All done
$('.demo-loader-content').fadeOut();
d.resolve();
} else if(data.can_continue !== 'yes') {
// *** All done; but is this an error condition? If so
// use `d.reject` instead of `d.resolve` below.
$('.demo-loader-content').fadeOut();
d.resolve();
} else {
demo_data.steps_to_take = data.remaining_steps;
worker(); // This is the internal recursive call
}
},
error: function (response) {
$('.demo-loader-content').fadeOut();
}
});
}
worker();
return d.promise();
}
This would be my approach, separating the individual AJAX requests from the looping over the content, and that also from the DOM updates:
function download_one_file(demo_data) {
return jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: setup_page_params.ajax_nonce,
content_install_request_data: JSON.stringify(demo_data),
}
});
}
function download_loop(demo_data) {
return download_one_file(demo_data).then(function(data) {
if (!data) {
return Promise.reject();
} else if (data.remaining_steps && data.remaining_steps.length) {
demo_data.steps_to_take = data.remaining_steps;
return download_loop(demo_data);
} else {
return Promise.resolve();
}
});
}
function download_required_files(demo_data) {
return download_loop(demo_data).finally(function() {
$('.demo-loader-content').fadeOut();
});
}

Sequentially chaining promises from 2 separate functions

I've reviewed all the related topics, but for some reason am unable to understand the correct syntax of resolving my case.
It's Javascript on Framework7 platform.
There is a function that has a $.each inside, which cycles through an array of items and executes an async POST operation to update that item.
There's a requirement to update items in two separate locations, so I execute this function twice, each time with a different arguments.
What would be the best approach to chain those two functions together in a sequence to be able to reload the page, as all async requests have been executed?
Thank you in advance!
UPDATE!
I managed to get this far, but promises.then doesn't result into "Success":
var tasks = [1,2,3];
var docs = [3,4,5];
var functions = [asyncOperation(tasks),asyncOperation(docs)]
var asyncOperation = function(items) {
return new Promise(function (resolve, reject) {
var deferreds = [];
items.forEach(function(i,e){
deferreds.push(
app.request({
url: buildurlnew,
type: "POST",
headers: buildheader,
contentType: "application/json;odata=verbose",
data: JSON.stringify(UpdatePayload),
success: function (data) {},
error: function(xhr, textStatus, errorThrown ) {}
)
});
return deferreds;
});
}
var promises = Promise.all(functions);
promises.then(function(results) {
//console.log("Success");
});
UPDATE 2 - The changed code, as per suggestions
var processtasks = function(array, dig) {
var getlistname = GetItemTypeForListName("Alerts")
var itemProperties = {'Title' :"Test"};
var UpdatePayload = {'__metadata': {'type': getlistname}};
for(var prop in itemProperties){
UpdatePayload[prop] = itemProperties[prop]
}
var buildurl = "<REST URL to Sharepoint list>"
var buildheader = { "Accept": "application/json;odata=verbose", "X-RequestDigest" : dig, "X-HTTP-Method": "MERGE", "If-Match": "*"}
return Promise.all(array.map(function(item) {
buildurlnew = buildurl+"("+item+")";
return app.request({
url: buildurlnew,
type: "POST",
headers: buildheader,
contentType: "application/json;odata=verbose",
data: JSON.stringify(UpdatePayload),
success: function (data) {},
error: function(xhr, textStatus, errorThrown) {}
});
}));
}
var processitems = function(listName, array, dig, type, source, web) {
var getlistname = GetItemTypeForListName(listName)
var buildurl = "<REST URL to Sharepoint list>"
var buildheader = { "Accept": "application/json;odata=verbose", "X-RequestDigest" : dig, "If-Match": "*"}
return Promise.all(array.map(function(item) {
var itemProperties = {'UserId' : app.data.UserID, 'Title' : item};
var UpdatePayload = {'__metadata': {'type': getlistname}};
for(var prop in itemProperties){
UpdatePayload[prop] = itemProperties[prop]
}
return app.request({
url: buildurl,
type: "POST",
headers: buildheader,
contentType: "application/json;odata=verbose",
data: JSON.stringify(UpdatePayload),
success: function (data) {},
error: function(xhr, textStatus, errorThrown ) {}
});
}));
}
processitems(listName, array, dig, type, source, web).then(function(r1) {
return processtasks(ids,dig);
}).then(function(r2) {
console.log(r2);
}).catch(function(err) {
console.log(err);
});
First you have to fix your asyncOperation() function because it is not properly returning a promise that is connected to the underlying async operations you're doing.
This code doesn't really make much sense. You have a basic structure of this:
var asyncOperation = function(items) {
return new Promise(function(resolve, reject) {
// body of function here
});
}
So far so good. But, what you need to do inside that promise executor function is start some async operation and call resolve or reject when you're done. You aren't doing that. Therefore, your promise never resolves or rejects.
Instead, you are returning an array of deferreds from the promise executor callback which does nothing. The promise executor function does not expect any return value so returning a value from it does nothing. You have to indicate completion of your asynchronous operation in the promise executor by calling either resolve(...) or reject(...).
If app.request() returns a promise, then you don't even need to make your own promise at all. You can just do something like this:
var asyncOperation = function(items) {
return Promise.all(items.map(function(item) {
return app.request(...);
}));
}
asyncOperation(...).then(function(results) {
// all done here
}).catch(function(err) {
// error here
});
items.map() generates an array of promises and Promise.all() returns a new single promise that monitors that array of promise and will resolve when all the promises in the array resolve or reject when any one of them rejects.
If app.request() does not return a promise, then you probably should make a "promisified" version that does so you can use it with promise functions like Promise.all(), perhaps using util.promisify().
To run two of these in parallel (which seems practical since they don't appear to depend upon one another), you could do this:
Then, once you are properly returning a promise for your function, if you have two of these, you can just use Promise.all() on more than one function call:
Promise.all([asyncOperation(...), asyncOperation(...)]).then(function(results) {
// both done here
// not the result may be an array of arrays
}).catch(function(err) {
// error here
});
What would be the best approach to chain those two functions together in a sequence
To run them in sequence, you can do this:
asyncOperation(args1).then(function(r1) {
return asyncOperation(args2);
}).then(function(r2) {
console.log(r2);
}).catch(function(err) {
console.log(err);
});

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>

javascript promise return recursively to index

Ok, so I've got a loop like so:
underscore.each(dom.paramArray, function(value, i) {
fetchDataFromServerWithParams(value, i);
});
In my current example it loops 3 times, idealy like: 0,1,2
However, when I log the index of the function called, it logs: 1,0,2, why ?
And how can I get it to call the function recursively, so first it will process the function with index:0, then index:1, and lastly, index:2
I think it has something to do with the functions I am calling (buildResult and buildSubResult), but Im really not sure?
The function that is called by the loop looks like:
function fetchDataFromServerWithParams(param, index) {
//Create promise
let getData = $.ajax({
headers: {
Accept : "text/plain; charset=utf-8",
"Content-Type": "text/plain; charset=utf-8"
},
type: "GET",
url: configuration.apiEndpoint,
data: { id: param},
dataType: "json"
});
//When done processing promise, build result
getData.then(function (data) {
let generatedData;
console.log(index);
if(index === 0) {
generatedData = buildResult(data);
} else {
$.each($("ul.material a"), function( index, value ) {
var target = $(this).parent();
if($(this).data("id") == param) { //Refactor later to ===
target.parent().addClass("open-folder");
target.parent().parent().find("ul").addClass("open-folder");
generatedData = buildSubResult(data, target);
}
});
}
}), function(xhr, status, error) {
// Handle errors for any of the actions
handleError(error);
};
}
you can use async library or any other library for this purpose
also you can change the line
getData.then(function (data) {
to
return getData.then(function (data) {
and use this code instead of your underscore loop
(function sequenceCall(index){
fetchDataFromServerWithParams(dom.paramArray[index], index).then(function(){
sequenceCall(index+1);
});
})(0);

AngularJS handle calling promise multiple times with some exceptions

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];
}

Categories