Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using ajax in a callback where the parent process itself relies on an ajax call

Tags:

I'm working with an application that uses DataTables to generate a HTML table which is populated by data from an ajax request.

This is fairly simple:

var substancesTable = $('#substancesTable').DataTable({
    "processing": true,
    "serverSide": true,
    "searching": false,
    "ajax": {
        "url": "/get-substances.json",
        "method": "POST",
        "cache": false,
        "dataSrc": function (json) {

            // Update non-Datatables UI elements and perform other functions based on the ajax response
            $('#numSubstances').html(json.recordsTotal);
            drawOptionsButton(json.isFiltering);

            // Must return data for DataTables to work
            return json.data;
        }
    },
    // ... 
});

There is a callback which DataTables provides, called rowCallback (https://datatables.net/reference/option/rowCallback) which allows post processing of table rows after the table has been drawn. The key thing here is that it's after the ajax request to /get-substances.json; the table must be populated with data because this callback is used to manipulate data within it at that point.

Within rowCallback I'm providing an array of row ID's in my table - that is to say ID's which correspond to <tr> elements inside #substancesTable - and I go on to expand these rows. I can do this manually by hardcoding in an array of row ID's, e.g.

 var substancesTable = $('#substancesTable').DataTable({
     // ...
     "rowCallback": function(row) {
       var id = $(row).find('td:first').text();
       var index = $.inArray(id, ['4', '7']); // hardcoded array

       if (index !== -1) {
           var tr = $(row).closest('tr');
           var row = substancesTable.row( tr );
           row.child.show();
           tr.addClass('active');
       }
 });

The array I've hardcoded means rows 4 and 7 are expanded after the table has been populated, which is equivalent to the user clicking on them.

The problem I have is that I don't want to hardcode the array. The application stores the equivalent of var index in Redis (cache) meaning that we can grab the data easily even if the user leaves the page. So I have added a second ajax request (outside the var substancesTable... block) to obtain the Redis data. This makes an ajax request to populate an array, activeRows:

var activeRows = [];
$.ajax({
    url: '/view-substance/get-active-rows',
    method: 'post',
    cache: false,
}).done(function(data) {
  activeRows = data;
  console.log(activeRows);
});

I understand that the nature of ajax means my code is asynchronous. In some cases the ajax request shown above will complete before the DataTable is drawn, so I get the console.log(activeRows) appearing before the table is rendered, and in other cases it happens afterwards.

What is the correct way to make this second ajax request such that the values from it can be used in place of the hardcoded array? I appreciate I will need to convert the response to an array (since it's still JSON in the console.log statement). But my question is focused on where to put this code such that it can be used reliably inside rowCallback?

I have read How do I return the response from an asynchronous call? and understand about the async nature. I can't work out how to structure this to be used in a callback that's already part of an ajax request.

The application uses DataTables version 1.10.16 and jquery 3.2.1

like image 933
Andy Avatar asked Nov 08 '18 10:11

Andy


2 Answers

You can actually use the ajax option to:

  1. Make the first AJAX request which retrieves the active rows.

  2. Once the active rows are retrieved, make the second AJAX request which retrieves the table data.

Example: (see full code and demo here)

var activeRows = [];

function getActiveRows() {
  return $.ajax({
    url: '/view-substance/get-active-rows',
    type: 'POST',
    dataType: 'json'
    ...
  }).done(function(data){
    activeRows = data;
    console.log(activeRows);
  });
}

function getTableData(data, callback) {
  return $.ajax({
    url: '/get-substances.json',
    type: 'POST',
    dataType: 'json',
    'data': data // must send the `data`, but can be extended using $.extend()
    ...
  }).done(callback); // and call callback() once we've retrieved the table data
}

$('#example').dataTable({
  ajax: function(data, callback){
    getActiveRows().always(function(){
      getTableData(data, callback);
    });
  },
  rowCallback: function(row, data){
    ...
  }
});

UPDATE

In the above example, I separated the AJAX calls into two different functions mainly to avoid long indentation in the ajax option when you call the $('#example').dataTable(). The code would otherwise look like:

var activeRows = [];

$('#example').dataTable({
  ajax: function(data, callback){
    // 1. Retrieve the active rows.
    $.ajax({
      url: '/view-substance/get-active-rows',
      type: 'POST',
      dataType: 'json'
      ...
    }).done(function(res){
      activeRows = res;
      console.log(activeRows);
    }).always(function(){
      // 2. Retrieve the table data.
      $.ajax({
        url: '/get-substances.json',
        type: 'POST',
        dataType: 'json',
        'data': data // must send the `data`, but can be extended using $.extend()
        ...
      }).done(callback); // and call callback() once we've retrieved the table data
    });
  },
  rowCallback: function(row, data){
    ...
  }
});

I used the .always() so that the table data would still be retrieved in case of failures in retrieving the active rows.

like image 63
Sally CJ Avatar answered Sep 20 '22 06:09

Sally CJ


You could solve your problem with Promises. Promises are objects that help you manage and coordinate asynchronous tasks. Your case would look something like this:

var activeRows = [];
var substancesTable = $('#substancesTable').DataTable({
     // ...
});

var pDataTable = new Promise(function(resolve, reject){
    // you wan't to resolve just once, 
    // after the table has finished processing the received data.
    // (You may want to change draw to an event that's more suitable)
    substancesTable.one('draw', resolve);
});

var pOpenRows = new Promise(function( resolve, reject ){

    $.ajax({
        url: '/view-substance/get-active-rows',
        method: 'post',
        cache: false,
    }).done(function(data) {
        // you can either save your rows globaly or give them to the resolve function
        // you don't have to do both
        activeRows = data; 
        resolve( data );
    });

});

// Here we basically create a third promise, which resolves (or rejects)
// automatically based on the promises in the array.
Promise.all([pDataTable, pOpenRows])
.then(function( values ){
    // expand your table rows here
    // Note: if you gave your rows to the resolve function you can access 
    // them here in values[1] (pDataTable's data would be in values[0])
});

In case you want to learn more about Promises:

  • https://developers.google.com/web/fundamentals/primers/promises
  • https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise
  • https://scotch.io/tutorials/javascript-promises-for-dummies

For details on browser support you may look here:

  • https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise
  • https://caniuse.com/#search=Promise
like image 22
Fitzi Avatar answered Sep 18 '22 06:09

Fitzi