forEach, async and callbacks - javascript

sorry if this question has been answered before but I couldn't find it.
I have an array of objects, and for each object I want to do an async call (ajax call), and when all async calls are finished, i want to call another function.
eg.
var list = [Object, Object, Object, Object];
var final= [];
$(list).each(function(){
//ajax call
getSomething(data, function(data){
final.push(data);
});
});
After all ajax calls are finished i wanna call function load(final);
Can this be done whith callbacks, and without libraries like when.js etc.
Th.

Call the function when the last item has arrived:
final.push(data);
if (final.length == list.length) load(final);

Since it looks like you have jQuery available, you can use the promises built into jQuery:
var list = [Object, Object, Object, Object];
var promises = [];
$.each(list, function(){
//ajax call
promises.push($.get(data));
});
$.when.apply($, promises).done(function() {
// all ajax calls done
// data from each ajax call is in arguments[0], arguments[1], etc...
load(arguments);
});
One other nice advantage of this mechansim vs. all the others shown so far is that this will keep the results in the order that you requested them, even if they don't come back in that order.
You can also provide a handler to .fail() in addition to .done() (or specify both with a .then(f1, f2)) if you want to catch the case where any ajax call fails.

This is a way to solve the problem with a simple counter
var counter = $(list).length;
$(list).each(function(){
$.get('URL', function(data){
/* do whatever you need for each list item */
if(--counter === 0){
/* Do whatever you wanted to do when all requests completed */
}
});
});

Fundamentally, you keep track of how many calls you've made and how many responses you've gotten, and when you've got all the responses, you call load(final). In your case, quite conveniently you have two arrays and are pushing the results of the calls based on the first array into the second, so you can compare their lengths. (Of course, you'll want to handle the error condition as well.)
Your quoted code is a bit suspect (I think you wanted $.each(list, ..., not $(list).each(...), but I think you probably meant something like this:
var list = [Object, Object, Object, Object];
var final= [];
$.each(list, function(data){
//ajax call
getSomething(data, function(result){ // <= I used `result` rather than `data`; using the same symbol in intermixed code like this is asking for trouble
final.push(result);
if (final.length === list.length) {
// All done
load(final);
}
});
});

Related

jQuery deferreds - order of execution in multiple blocks

NOTE
This problem somehow happens only with a specific server-side api. Therefore it addresses the wrong problem. I'll not delete it since it have answers and comments.
I'm trying to execute a few ajax requests, do some stuff after each is done and some other stuff after all of them are done, for that I'm using the code below:
let
myarr = [],
myfunc = arg => myarr.push(arg);
$.when(
$.post(myparams).done(myfunc),
$.post(otherparams).done(myfunc),
$.post(yetanother).done(myfunc)
// it comes out with only one arg
).then(e => console.log(myarr));
But when it comes to execute the then block it usually has only executed the done of the first operation, how could I fix that?
I'm sorry if it's a duplicate but honestly I didn't even knew what to search for :/
Comment
I also tried to create my own deferreds where I would execute the ajax and resolve them inside the done block, but that yielded the same results.
Using only done or only then, same.
Per jQuery's documentation on $.when():
Each argument [of .then()] is an array with the following structure: [ data, statusText, jqXHR ]
Meaning you could do something like this...
$.when(
$.post(myparams),
$.post(otherparams),
$.post(yetanother)
).then((res1, res2, res3) => { //Arg for each result
myfunc(res1[0]); //Call myfunc for result 1's data
myfunc(res2[0]); //Call myfunc for result 2's data
myfunc(res3[0]); //Call myfunc for result 3's data
});
Though perhaps a cleaner version might be something like this instead...
let
myarr = [],
myfunc = arg => myarr.push(arg);
$.when(
$.get('https://jsonplaceholder.typicode.com/todos/1'),
$.get('https://jsonplaceholder.typicode.com/todos/2'),
$.get('https://jsonplaceholder.typicode.com/todos/3')
).then((...results) => { //Get all results as an array
results.map(r=>r[0]).forEach(myfunc); //Call myfunc for each result's data
console.log(myarr);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

What is the correct method of making json requests and storing values while looping through an array?

I would like to loop through an array of addresses so I can request their respective geolocations using the google maps geocode api. I'm having trouble pushing the results (lat/long coordinates) into an array. It seems like the console.log function below is firing before the code above it is finished executing.
I would appreciate it if someone could point this javascript noob in the right direction.
function getCoords() {
$.each(addressList, function(index, val) {
$.getJSON('https://maps.googleapis.com/maps/api/geocode/json?address=' + val + '&key=apikey').done(function(data){
coordsList.push(data.results[0].geometry.location);
});
});
console.log(coordsList);
}
You're mixing synchronous and asynchronous code. As you suspected, your console.log is firing before the requests have completed.
jQuery's deferred objects are good for this sort of thing - i.e. do something when a bunch of asynchronous code has completed.
var reqs = []; //set up a container to log our async requests to Google
$.each(addressList, function(index, val) {
reqs.push($.getJSON('https://maps.googleapis.com/maps/api/geocode/json?address=' + val + '&key=apikey').done(function(data){
coordsList.push(data.results[0].geometry.location);
}));
});
//when ALL async requests have completed, console.log.
$.when.apply(this, reqs).done(function() { console.log(coordsList); });
The need for apply() is a little outside the scope of this question if you're a JS noob, but essentially it's a way of passing an array to a function that expects not an array but separate arguments. $.when() expects requests passed as separate arguments, i.e. $.when(req1, req2, ...) but our requests are in an array; apply() gets round this. Here's more on apply().

Accessing Object properties when the object is within an array (javascript)

I am attempting to create an array of objects and then access object properties within the array, but it comes back undefined. I call the createObjArray() function and immediately after I do a console.log(objArray[1]); and it prints out the object with all it...s properties just fine. However, if I attempt to do console.log(objArray[1].name); firebug prints "undefined". Also, when stepping through my code in firebug I can mouse over objArray[1].name and it displays the correct name. What is happening here, it's driving me nuts.
var objArray = [];
function createObjectArray(numOfObjs) {
for(var i=0; i<numOfObjs; i++) {
packages.push(initObj(i));
}
}
function initObj(i){
var newPackage;
var p = {};
$.getJSON('.../package' + i + '.json', function(data) {
newPackage = new Package(data);
p.name = newPackage.name;
p.id = i;
});
return p;
}
This will work:
var objArray = [];
function createObjectArray(numOfObjs, callback) {
var filledPackage = [];
var nbLeft = numOfObjs;
for(var i=0; i<numOfObjs; i++) {
initObj(i, function(p){
filledPackage.push(p);
nbLeft--;
if (nbLeft === 0){
callback(filledPackage);
}
});
}
}
function initObj(i, callback){
var newPackage;
var p = {};
$.getJSON('.../package' + i + '.json', function(data) {
newPackage = new Package(data);
p.name = newPackage.name;
p.id = i;
callback(p);
});
}
//Get a filled object array:
createObjectArray(5, function(filledArray){
objArray = filledArray;
//Code here will be executed AFTER all the $.getJSON queries have returned.
//objArray is not empty.
});
//Code here will be executed WHILE the getJSON queries are running and
//while objArray is still empty. Due to the way the JS event loop works,
//it is impossible that code placed here will be able to use the content
//of objArray unless you call an async function such as anything AJAX or
//setTimeout, but that's iffy. Code you want to be executed once objArray
//has been filled should be inside of the callback above.
The problem is that $.getJSON is aynchronous, meaning that it doesn't automatically returns a result. Instead, you give it a callback. A callback is a function to execute once it has received a result. In this case, the callback is the anonymous function created when calling $.getJSON. That callback receives the result from the server, adds it to the array and then checks if the array has been filled. Since we're doing async code due to the $.getJSON function, we must return the result asynchronously too. To do so, we demand the initObj function to receive a function to call once it has completed (another callback). We call that callback and pass it the parameter. We then return the filled array through a callback once again.
Your call to $.getJSON is asynchronous. When initObj() returns p it is still an empty object.
However initObj() creates a closure which captures a reference to p so when $.getJSON returns p is populated.
This is why the object seems empty in code you run immediately after populating the array. However by the time you run your console command the asynchronous calls have returned and the objects are populated.
You need to wait for all your async calls to return before continuing work on the array. One way to do this would be to increment a counter when you make a call and decrement it when a call returns, then when the final call returns the counter would drop to zero and you continue processing.
Alternatively you could setup a setTimout loop to keep polling the array the check when all its items are populated.
Both approaches are risky if you think one of the calls might fail, but the approach itself is fundamentally risky as you are making multiple ajax calls so you have to handle multiple possible failures. It would be a lot cleaner to grab all the data in one go so you can handle success / error states once in the success / error handler in jQuery.ajax.

how to do a custom callback on a user function in javascript

I use callbacks all the time when using 3rd part libraries such as jquery, however I've run into an issue where I need to set up my own call back. Take for instance my current code:
// Get All Rates
function getAllRates() {
$('#shiplist tr.mainRow').each(function() {
var curid = $(this).attr("id");
var cursplit = curid.split("_");
var shipid = cursplit[1];
getRate(shipid);
});
}
This iterates through all the table rows that have class "mainRow", and each main row has the id of "shipment_#####" so for each row it splits by the _ to get the actual ship id, which it then uses to calla "getRate" function which sends an ajax call to the server using UPS or USPS api's to get the rate for the given shipment.
This works fine for UPS all rows start loading at once and return their rates independently. The problem is now with Stamps.com, they use an authenticated soap conversation so an authenticator string needs to be received from the first call and used in the next call and so on.
So basically I need to modify the above function to execute the "getRate()" function and wait for it to complete before executing the next iteration.
Anyone know how I can do this with out a lot of fuss?
EDIT - Clearification on the question being asked:
I want to take the following function:
getRate(shipid);
and access it like so:
getRate(shipid, function(shipList) { _Function_data_here_ });
When I define the getRate function, how do I define it so that it has that callback ability? how does that work?
I would suggest you extract all the IDs at once and pass them to your function. That way, you can easily process the first on individually and the rest afterwards. With deferred objects, this is really easy.
For example:
function extractAllIds(callback) {
var ids = $('#shiplist tr.mainRow').map(function() {
return this.id.split('_')[1];
}).get();
callback(ids);
}
function getRates(ids) {
var first = ids.shift();
$.ajax({data: first, ...}).done(function(response) {
// extract the authenticator string
for(var i = ids.length; i--; ) {
$.ajax({data: ids[i], ...});
}
});
}
extractAllIds(getRates);
In any way, you cannot make $.each wait for response of Ajax calls.

Synchronous function calls involving post json calls where one function should succeed upon the success of another function

I have two functions one of which includes multiple json call which are post by nature.
I want these to be synchronous. That is, one should run only upon the completion of the previous post (and if all posts are done and successful I want the second function to fire).
The code structure is somewhat like this:
$.getSomeData = function() {
$.postJSON("iwantdata.htm",{data:data},function(data)){
});
$.postJSON("iwantmoredata.htm",{data:data},function(data)){
});
});
$.useSomeData = function() {
});
The useSomeData must work upon subsequent json calls.
Can anyone please help me? Thanks in advance.
So basically you want something like this:
function chainPost(url1, url2, initialInput, func) {
$.post(url1, {data: initialInput})
.done(function (initialOutput) {
$.post(url2, {data: initialOutput})
.done(function (secondOutput) {
func(initialOutput, secondOutput);
});
});
}
chainPost("iwantdata.htm", "iwantmoredata.htm", 0, function (first, second) {
alert(first);
alert(second);
});
You can just nest them, starting the 2nd one in the completion function of the first and so on:
$.getSomeData = function() {
$.postJSON("iwantdata.htm",{data:data},function(data) {
$.postJSON("iwantmoredata.htm",{data:data},function(data)){
// use the data here
});
});
};
When dealing with asychronous functions, you cannot write code such as:
$.getSomeData();
$.useSomeData();
By definition, the first is asynchronous so it will not have completed yet with the second function is called and javascript does not have the ability to stop JS execution until an asynchronous operation is done.
You could pass your use function to the get function and then it would get called when the data was available as an addition to the above example like this:
$.getSomeData = function(fn) {
$.postJSON("iwantdata.htm",{data:data},function(data) {
$.postJSON("iwantmoredata.htm",{data:data},function(data)){
fn(data);
});
});
};
Then, you'd have a getSomeData(useFn) function that would take an argument of the function to call when all the data was ready.
Deferred objects [docs] are perfect for this. Unfortunately, your code example contains syntax errors and it is not clear how the calls are nested. So, I'm not sure if you want to run both Ajax calls after one another or parallel, but either way is possible.
Here are two examples. Have a look at the documentation for more information and play around with it.
Note: .postJSON is not a built in jQuery method, I assume here that you are returning the return value from the $.ajax (or $.post) function.
Parallel Ajax calls:
$.getSomeData = function() {
var a = $.postJSON("iwantdata.htm", {data:data});
var b = $.postJSON("iwantmoredata.htm", {data:data});
// return a new promise object which gets resolved when both calls are
// successful
return $.when(a, b);
};
// when both calls are successful, call `$.useSomeData`
// it will have access to the responses of both Ajax calls
$.getSomeData.done($.useSomeData);
See: $.when
Chained Ajax calls:
... where the response of the first call is the input for the second one. This is only an example, of course you can pass any data you want.
$.getSomeData = function() {
return $.postJSON("iwantdata.htm", {data:data}).pipe(function(response) {
// execute the second Ajax call upon successful completion
// of the first one
return $.postJSON("iwantmoredata.htm", {data:response});
});
};
// if both Ajax calls are successful, call `$.useSomeData`
// it will have access to the response of the second Ajax call
$.getSomeData.done($.useSomeData);
See: deferred.pipe()
If you have a more complex logic, you can also create, resolve or reject your own deferred objects. Have a look at the examples in the documentation.

Categories