Julius R. Julius R. - 1 month ago 8
Ajax Question

Multiple nested AJAX calls in jQuery: How to do it right, without falling back to synchronous?

I know, there already are a lot of questions concerning AJAX and Synchronicity here, but I somehow did not find the right answer to my cause. Please be gentle, I am a noob, so, this might be a duplicate. Still, even hinting to that might help me and others finding an appropriate answer.

The Function

$.fn.facebookEvents = function(options){
var fbEvents = 'https://graph.facebook.com/'+options.id+'/events/?access_token='+options.access_token+'&since=now&limit=500';
var events = [];

$.when($.getJSON(fbEvents)).then(function(json){

$.each(json.data, function(){
$.getJSON('https://graph.facebook.com/'+this.id+'/?access_token='+options.access_token, function(jsonData){
events.push(jsonData);
console.log(events); // here array "events" is filled successively
});
console.log(events); // here array "events" remains empty
});

}).done(function(){

$("#fb_data_live").html(
$("#fb_events").render(events)
);

});
};


What happens in this function, is an AJAX Call (via jQuerys getJSON shorthand) for the event list of a Facebook Page. This will return a list of JSON objects representing each date. Example of one object as response:

{
"end_time": "2017-03-16T23:00:00+0100",
"location": "Yuca K\u00f6ln",
"name": "K\u00f6rner // G\u00e4nsehaut Tour 2017 // K\u00f6ln",
"start_time": "2017-03-16T20:00:00+0100",
"timezone": "Europe/Berlin",
"id": "985939951529300"
},


Unfortunately, these are missing the details we need. They are hidden (nested) and can be found, after making a second getJSON (line 8 in the function), using the individual "id" of each object. Now Facebook replies:

{
"description": "http://www.eventim.de/koerner\nTickets ab sofort exklusiv auf eventim.de und ab MI 07.09. \u00fcberall wo es Tickets gibt sowie auf contrapromotion.com.",
"end_time": "2017-03-16T23:00:00+0100",
"is_date_only": false,
"location": "Yuca K\u00f6ln",
"name": "K\u00f6rner // G\u00e4nsehaut Tour 2017 // K\u00f6ln",
"owner": {
"name": "K\u00f6rner",
"category": "Musician/Band",
"id": "366592010215263"
},
"privacy": "OPEN",
"start_time": "2017-03-16T20:00:00+0100",
"timezone": "Europe/Berlin",
"updated_time": "2016-09-28T10:31:47+0000",
"venue": {
"name": "Yuca K\u00f6ln"
},
"id": "985939951529300"
}


Et voila, the details I was looking for. After making this second (nested) AJAX call, I push the JSON data in the array "events" (line 12).

The Problem

This should fill up the array with every .each iteration. However, take a look at the two 'console.logs'. The first one returns the filled array, the second one returns an empty array... If the AJAX calls are being made synchronously (which is not how it is supposed to be), e.g. by adding
$.ajaxSetup({async: false});
prior to the function, it works fine however. The array is filled and can be rendered (line 18).

The Question

How can this behaviour be explained and how can this function be done right? I know, there must be a way using deferred and promises? I thought i did, by using .when.then.done... obviously I erred.

With best regards,
Julius

Answer

Obviously you don't know nothing about promises. I guess I would have done it some other way, but using promises, you could do the following:

$.fn.facebookEvents = function(options){
    var fbEvents = 'https://graph.facebook.com/'+options.id+'/events/?access_token='+options.access_token+'&since=now&limit=500';
    var events = [];
    var deferred=$.Deferred();
    var promises=[];
    var done=false;

    $.when(deferred).then(function(events) {
      console.log(events);
    });

    $.getJSON(fbEvents).done(function(json){
        $.each(json.data, function(){
            promises.push($.getJSON('https://graph.facebook.com/'+this.id+'/?access_token='+options.access_token).done(function(jsonData){ 
                events.push(jsonData);
                var completed=promises.filter(function(element) { return element.state=='pending' }).length==0;
                if(done&&completed) {
                  deferred.resolve(events);
                }
            }));
        });
        done=true;
    });
};