Orchestrating Nested AJAX Calls Within Each Loop Before Chart Is Drawn (JQuery)

1.1k Views Asked by At

I've been looking for an answer for this a couple of days now without luck.

I'm trying to build a chart from data collected via ajax calls to JSON REST Api. I use morris.js for building my charts. The data for chart is collected via 3 nested ajax calls. The first ajax call retrieves a list with tasks, on success a loop iterates over the result and makes an ajax call for every result. And on success on these calls data is retrieved to build the chart.

Inside the last success function an for loop runs to build an array containing the data for the chart.

These data get pushed into the parameter array for the chart (morris.js).

So 3 nested ajax call, with an $.each loop after the first call.

My problem is that the charts gets build for each ajax iteration. I have been trying to move the chart function (Morris.Donut(params);) outside of loop without luck.

My code:

var arr = [];
var counts = [];
var test = [];
$.ajax({
    url: "http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests?$filter=TaskStatusValue%20eq%20%27Not%20Started%27",
    headers: {
        'accept': 'application/json;odata=verbose',
        'content-type': 'application/json;odata=verbose'
    },
    success: function(data) {
        $.each(data.d.results, function(a, data) {
            $.ajax({
                url: "http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests(" + data.RequesterId + ")/CreatedBy",
                headers: {
                    'accept': 'application/json;odata=verbose',
                    'content-type': 'application/json;odata=verbose'
                },
                success: function(data2) {
                    $.ajax({
                        url: "http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests(" + data.AssignedToId + ")/AssignedTo",
                        headers: {
                            'accept': 'application/json;odata=verbose',
                            'content-type': 'application/json;odata=verbose'
                        },
                        success: function(data3) {
                            var params = {
                                element: 'taskchart',
                                data: [],
                                colors: ['#b85f28', '#d46125', '#CE4E00']
                            };
                            $(".inner").prepend('<p>'+data.Request +' <br>Submitted by: '+data2.d.Name+'<br>Assigned to: '+data3.d.Name+' | Due in: '+data.DueInDays+' day(s)</p>');
                            var indexOfName = arr.indexOf(data3.d.Name);
                            if (indexOfName == -1) {
                                arr.push(data3.d.Name);
                                counts.push(1);
                            } else {
                                counts[indexOfName] ++;
                            }
                            for (var i in arr, counts) {
                                test = {
                                    'label': '' + arr[i] + '',
                                    'value': counts[i]
                                }
                                params.data.push(test);
                            }
                            Morris.Donut(params);
                        }
                    })
                }
            })
        })
    }
})

I know this code can look a bit messy, but I can't seem to build it better. Any suggestions on how I can build the chart after the ajax calls have finished loading the data?

I'm really banging my head against the wall here.

2

There are 2 best solutions below

0
On BEST ANSWER

Quick Rundown of The Problem

After looking through your codes more closely, the reason why your chart is re-built for each iteration in your $.each function is because your Morris.Donut(params); is called within the deepest nested success callback in your loop (duh?). And moving Morris.Donut(params); outside of your $.each function won't work simply because of the asynchronous nature of your nested ajax calls, i.e. the call to $.each probably exits and Morris.Donut(params); is invoked, before the ajax calls are completed.

An Easier Alternate (Server-Side) Solution

Before showing you some javascript codes, I recommend that if you have control over the server-side API codes, you should consider moving all these complexity there; i.e. create a new web service that accepts an TaskStatusValue input, and returns IT requests that satisfy this condition along with whatever other information (requester's name, assignee's name etc.) is needed to construct your charts. I think essentially, that's what your JS codes are trying to do.

As I mentioned in my previous comment, your nested AJAX calls rely on results of the parent AJAX call, which essentially, boils down to making synchronous requests, defeating the purpose of asynchronous calls.

The JQuery Solution (TL;DR)

With that being said, if you absolutely have to do everything on the client side, we will need to pass an array of jQuery.deferred objects to the jQuery.when function such that your chart will be drawn only after all your asynchronous calls are completed.

There can be other easier, better way to do this. But here's my solution....

Code Refactoring

Extract your two nested AJAX calls into one separate function like the following. This function will return the Deferred object returned by the $.ajax function:

function retrieveITRequestsFrom(url){
  return $.ajax(url, {
           headers: {
             'accept': 'application/json;odata=verbose',
             'content-type': 'application/json;odata=verbose'
           },
           /* etc */
         });
}

After getting all the IT requests with TaskStatusValue=NotStarted in your first success callback, add all the AJAX (Deferred objects) calls you will be making later to an array like this:

deferreds = []
$.each(data.results, function(){
    var requesterURL = 'http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests(' + this.RequesterId + ')/CreatedBy';
    deferreds.push(retrieveITRequestsFrom(requesterURL));

    var assigneeURL = 'http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests(' + this.AssignedToId + ')/AssignedTo';
    deferreds.push(retrieveITRequestsFrom(assigneeURL));        
});

Then finally, you can pass this array of Deferred objects to the $.when function.

$.when.apply($, deferreds)
 .then(function(){
     $.each(arguments, function(){
         console.log(this[0]);
     });
})

The pseudo-array arguments within the $.each function should contain all the results gathered from all your ajax calls. You can possibly draw your chart within the then callback.

I experimented with this using this fiddle, using some fake data.

1
On
  1. Change the last two ajax calls as synchronous (by adding option async:false)
  2. Move the params object to the declaration section
  3. Move the morris.donut(params) line to the end of the 1st ajax call success function I have change the whole code block from the above (you question's code) with these changes
var arr = [];
    var counts = [];
    var test = [];
    var params = {
        element: 'taskchart',
        data: [],
        colors: ['#b85f28', '#d46125', '#CE4E00']
    };
    $.ajax({
        url: "http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests?$filter=TaskStatusValue%20eq%20%27Not%20Started%27",
        headers: {
            'accept': 'application/json;odata=verbose',
            'content-type': 'application/json;odata=verbose'
        },
        success: function(data) {
            $.each(data.d.results, function(a, data) {
                $.ajax({
                    async: false,
                    url: "http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests(" + data.RequesterId + ")/CreatedBy",
                    headers: {
                        'accept': 'application/json;odata=verbose',
                        'content-type': 'application/json;odata=verbose'
                    },
                    success: function(data2) {
                        $.ajax({
                            async: false,
                            url: "http://helpdesk.site.com/IT/_vti_bin/ListData.svc/ITHelpdeskRequests(" + data.AssignedToId + ")/AssignedTo",
                            headers: {
                                'accept': 'application/json;odata=verbose',
                                'content-type': 'application/json;odata=verbose'
                            },
                            success: function(data3) {

                                var indexOfName = arr.indexOf(data3.d.Name);
                                if (indexOfName == -1) {
                                    arr.push(data3.d.Name);
                                    counts.push(1);
                                } else {
                                    counts[indexOfName] ++;
                                }
                                for (var i in arr, counts) {
                                    test = {
                                        'label': '' + arr[i] + '',
                                        'value': counts[i]
                                    }
                                    params.data.push(test);
                                }

                            }
                        })
                    }
                })
            })
             Morris.Donut(params);
        }   
    })