0

I need to fetch data (Q & A) from a distant service using their API. Data is split in different categories and the only methods they offer allow me to list categories and items from a specific category. However, My briefing implies that I gather data from different categories. I ended up doing this (this will gather data from all categories):

var getEveryQA = function(sLang)
{
    var allQA = [];
    //This request is for getting category listing
    $.ajax({
            crossDomain: true,
            contentType: "application/json; charset=utf-8",
            url: category_list_URL,
            data: { Lang: sLang }, 
            dataType: "jsonp",
            success: function(responseData){
                for (var i = 0; i < responseData.length; i++) 
                {
                    if(responseData[i].Code.toLowerCase !== "all")//category "all" has no real existence although it is returned in categories listing
                    {
                        //Request items for each category
                        $.ajax({
                            crossDomain: true,
                            contentType: "application/json; charset=utf-8",
                            url: items_by_category_URL,
                            data: { Lang: sLang, Category: responseData[i].Code }, 
                            dataType: "jsonp",
                            success: function(responseData){
                                    allQA = allQA.concat(responseData);//object from this response will be concatenated to the global object
                                }
                        });

                    }
                }
            }
        }); 
}

What I would like is to trigger a sorting method whenever all the AJAX calls done in my for loop have succeeded. I've the feeling jQuery's deferred is the solution, but the many examples I've read weren't compatible with my for... loop structure. Is there a way to build some kind of "queue" out of my multiple callbacks that I could then pass as an argument to the deferred method ? Or maybe am I looking in the wrong direction ?

Laurent S.
  • 6,816
  • 2
  • 28
  • 40
  • 1
    Use `$.when`. Place your ajax calls in an array, then call `$.when(thatarray).then(function(){Do something with all the results});` You can add the results in another array (as the come back in any order) ans sort them in the `then` function. – iCollect.it Ltd Sep 22 '14 at 14:07
  • @TrueBlueAussie keep in mind, `$.when` doesn't work with an array unless you also use .apply – Kevin B Sep 22 '14 at 14:24
  • @KevinB Try at `console` , this page : `var q = []; q.push($.ajax(), $.ajax()); $.when(q).done(function(data) {console.log(data)}) // [Object, Object]` . fwiw, alternatively , `var dfd = new $.Deferred(); dfd.resolveWith($, [$.ajax(), $.ajax()]); dfd.done(function(data) { console.log(data, data)})` – guest271314 Sep 22 '14 at 14:48
  • @guest271314 $.when, when given a non-promise, will resolve immediately. – Kevin B Sep 22 '14 at 14:49
  • @guest271314 Here's an example: http://jsfiddle.net/17a1uhms/2/ Notice how console.log 3 happens immediately rather than after 2 seconds, and how console.log 4 happens 2 seconds later due to proper use of $.when with .apply – Kevin B Sep 22 '14 at 14:55
  • @KevinB At jsfiddle , actually does log in order , (not certain if that is expected result ?) ; `1` (new line) , `2` (new line) , `3` . yes, consideration of non-`promise` object/values, etc. resolving immediately perhaps required ; though should be able to cast an object to a `promise` a couple manners , including `dfd.promise(obj); obj.done(fn)` – guest271314 Sep 22 '14 at 15:02
  • Yes, it logs in order, but it isn't actually waiting for the promises to complete. 1 2 and 3 are all happening instantly, where as 3 is supposed to be waiting 2 seconds just like 4 is. If you increased 3's delay to 10 seconds, it will still log before 4 which waits for 2 seconds. – Kevin B Sep 22 '14 at 15:03
  • Here: http://jsfiddle.net/17a1uhms/3/ the expected output based on you and trueblueaussie would be 1243, but it actually logs 1234. – Kevin B Sep 22 '14 at 15:06
  • Didn't include any `deferred` pieces at Answer , here, as evaluating `length`s of arrays involved appeared sufficient to meet requirement . `$.when()` , `.resolve()` , and `.promise()` should each accept an array of `x` , without including `apply` ; in particular , `.resolveWith` should have similar functionality as `$.when.apply` . Again , gather point of what presenting as to differences between `promise` object and non-`promise` object as to async processing. – guest271314 Sep 22 '14 at 15:15
  • @KevinB Utilizing `$.queue()` to process either `promise` or non-`promise` objects , perhaps provides greater versatility - and does not require casting a non-promise object to a promise object. e.g., see link at Answer; at that piece , utilized a `$.Deferred()` , wrapped in `$()` to provide `$.queue()` functionality , then when all tasks completed , unwrapped the `$()` to utilize the `$.Deferred()` beneath. the primary processing mechanism was the `$.queue` though. – guest271314 Sep 22 '14 at 15:23
  • I prefer to use the native promise interface, or a separate one than jQuery so that you get proper error handling through .catch. But to each his own. The problem with queue is it only sends one request at a time, potentially increasing the overall amount of time it takes to get all of the data. – Kevin B Sep 22 '14 at 15:26
  • With the native interface and most others, you get this method called `all` that DOES accept an array of promises. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all – Kevin B Sep 22 '14 at 15:28
  • Yes. Both native promises and jquery promises could be further modified , or adjusted to meet requirements of author. Could send multiple requests/functions/tasks through `$.queue` at single function - if required or desired. Could also attach a `.catch` to end of jquery promise/deferred. here is a piece adding a `when` and `done` to native promises http://stackoverflow.com/a/23587868/2801559 . Thanks ! – guest271314 Sep 22 '14 at 15:30
  • Actually tried `all` recently , seemed to return different results if there was an error in the array chain ? If no error , results processed through callback , if single error in the chain , only error callbacks called ? Again, either could be adjusted / modified to meet authors' requirements. – guest271314 Sep 22 '14 at 15:37
  • Yes, that's how it works. If any error occurs, you won't get to the done callbacks, instead you will immediately go to catch. – Kevin B Sep 22 '14 at 15:43
  • @KevinB Composed piece linked at post to avoid that model. Instead all `request` functions are processed , then when `requests` resolved , can filter through either success or error return values ; do stuff with the collection of any return values at `requests.done` . For example , if change one of the `url`s at object containing them to `/echo/jsons/` , that `error` will not stop the chain. Can then filter through the collection at the `requests.done` and do stuff with either `error` or `success` data. Alternatively, could also preserve that model by attaching a `fail` to `request` function. – guest271314 Sep 22 '14 at 15:55
  • Right, it all depends on what you want to do. There are hundreds of different ways to attack this problem. – Kevin B Sep 22 '14 at 15:56
  • Thanks for your help. I see it's not a so simple problem :-) – Laurent S. Sep 23 '14 at 19:12

2 Answers2

3

Edit

That would have been a good (and simpler) approach but I don't think it will work. allQA will consist of all the QA of all categories, and therefore the sort would occur after the first iteration (when all we have pushed into that array are the first results) while I need it to occur when allQA is "full" –Bartdude

Note $.ajax() call's response variable in for loop : _responseData (see leading _ underscore) - different variable than the initial $.ajax() response responseData (no leading underscore) .

The sort , or, other tasks, should only occur once allQA.length === responseData.length -- not _responseData.length (which is individual $.ajax() responses concatenated within for loop).

Adding , including additional deferred or promise pieces not needed to accomplish task described at original Question -- unless Answer specifically require solving utilizing deferred or promise methods.

length properties of objects or arrays involved should be sufficient to accomplish task described at OP.

Example piece at jsfiddle http://jsfiddle.net/guest271314/sccf49vr/ , utilizing same pattern as originally posted :

var arr = [7, 5, 6, 1, 8, 4, 9, 0, 2, 3]; // unsorted array
$("body").append(arr + "<br />");
var getEveryQA = function (sLang) {
    var allQA = [];
    //This request is for getting category listing
    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        url: "/echo/json/",
        data: {
            json: JSON.stringify(sLang)
        },
        dataType: "json",
        success: function (responseData) {
            for (var i = 0; i < responseData.length; i++) {
                // category "all" has no real existence although 
                // it is returned in categories listing
                if (responseData[i] !== "all") {
                    //Request items for each category
                    $.ajax({
                        type: "POST",
                        contentType: "application/json; charset=utf-8",
                        url: "/echo/json/",
                        data: {
                            json: JSON.stringify(responseData[i])
                        },
                        dataType: "json",
                        // note leading `_` underscore at `for` loop
                        // `_responseData` -- different than initial 
                        // `responseData` response from first request
                        success: function (_responseData) {
                            //object from this response will be concatenated to the global object
                            allQA = allQA.concat(_responseData);
                            // do stuff when `allQA` length === `responseData` (for loop) length
                            console.log(allQA.length === responseData.length);
                            if (allQA.length === responseData.length) {
                                // do sorting stuff
                               allQA.sort(); // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
                               console.log(allQA, arr);
                               $("body").append(allQA.join(",")); 
                            }
                        }
                    });
                }
            }
        }
    });
};

getEveryQA(arr);

Try

for (var i = 0; i < responseData.length; i++)  {
  // category "all" has no real existence although 
  // it is returned in categories listing
  if(responseData[i].Code.toLowerCase !== "all") {
      //Request items for each category
      $.ajax({
        crossDomain: true,
        contentType: "application/json; charset=utf-8",
        url: items_by_category_URL,
        data: { Lang: sLang, Category: responseData[i].Code }, 
        dataType: "jsonp",
        success: function(_responseData){
          //object from this response will be concatenated to the global object
          allQA = allQA.concat(_responseData);
            // do stuff when `allQA` length === `responseData` (for loop) length
            if (allQA.length === responseData.length) {
              // do sorting stuff
            }
         }
     });    
   }
};
guest271314
  • 1
  • 15
  • 104
  • 177
  • That would have been a good (and simpler) approach but I don't think it will work. `allQA` will consist of all the QA of all categories, and therefore the sort would occur after the first iteration (when all we have pushed into that array are the first results) while I need it to occur when `allQA` is "full" – Laurent S. Sep 23 '14 at 07:14
  • @Bartdude Note leading `_` underscore at `$.ajax()` calls response in `for` loop. Includes leading `_` underscore , differentiating those variables from initial `responseData` (no leading `_` underscore) object. See updated post , jsfiddle . Thanks – guest271314 Sep 23 '14 at 15:46
  • Indeed ! My bad ! That's an upvote then as we can't validate 2 answers and the answer from TrueBlueAussie lead me to learn about ´when()´ and ´apply()´ functions and looks like a more elegant pattern, although more complex to read back from where I see it. – Laurent S. Sep 23 '14 at 19:07
2

From comment: Use $.when. Place your ajax calls in an array, then call $.when.apply(thatarray).then(function(){Do something with all the results});

You add all your results into another array already (as they come back in any order) and sort them etc in the then function:

var getEveryQA = function(sLang)
{
    var allQA = [];
    //This request is for getting category listing
    $.ajax({
            crossDomain: true,
            contentType: "application/json; charset=utf-8",
            url: category_list_URL,
            data: { Lang: sLang }, 
            dataType: "jsonp",
            success: function(responseData){
                var requests = [];
                for (var i = 0; i < responseData.length; i++) 
                {
                    if(responseData[i].Code.toLowerCase !== "all")//category "all" has no real existence although it is returned in categories listing
                    {
                        //Request items for each category
                        requests.push($.ajax({
                            crossDomain: true,
                            contentType: "application/json; charset=utf-8",
                            url: items_by_category_URL,
                            data: { Lang: sLang, Category: responseData[i].Code }, 
                            dataType: "jsonp",
                            success: function(responseData){
                                    allQA = allQA.concat(responseData);//object from this response will be concatenated to the global object
                                }
                        }));
                    }
                }
                $.when.apply(requests).then(function(){
                    // Do something with your responses in allQA
                });
            }
        }); 
}

the return value from $.ajax is a promise that can be aggregated with other promises. $.when is about the only method that can take an array of promises and let you do things after all complete (or any fail).

iCollect.it Ltd
  • 92,391
  • 25
  • 181
  • 202
  • I prefer the .when solution, but.. you haven't used it properly. $.when does not accept an array of promises unless you use .apply to apply the array as multiple arguments to .when. – Kevin B Sep 22 '14 at 14:58
  • Technical correction. `$.when()` doesn't take an array. It takes multiple arguments that are promises (a huge oversight in design in my opinion, but that is the way it is). You would have to use `$.when.apply($, array)` to pass it an array. – jfriend00 Sep 23 '14 at 02:40
  • This solution coupled with the use of `apply()` as suggested by jfriend00 seems to do the trick. I was close to it but still it made me spare quite some time ! I will accept this solution as soon as it will use the `apply()` method on the last instruction. – Laurent S. Sep 23 '14 at 07:32
  • Updated. I should have remembered that, but don't use it this way often. Cheers. – iCollect.it Ltd Sep 23 '14 at 08:25
  • Validated ! But please update also the beginning of your answer accordingly so that it's perfectly correct :-) thanks for your help and the learnings. – Laurent S. Sep 23 '14 at 19:10
  • @Bartdude: Do'h! Well spotted. Also fixed :) – iCollect.it Ltd Sep 24 '14 at 14:49