0

For this project I am working on, I am pulling data via REST API to populate my DataTable. To reduce the amount of similar items displayed, I want to create DataTable subrows with a drop down based on an item in my "Deliverables" Column. The example I want to base it off is the example table shown here.

I have the column Deliverables with two options, Meeting Minutes and MSR, I want to have only those two things populate to the table, then have a click down showing everything under what "Program" it is. Here is a picture of my current DataTable, and I will attach the code below.


Here is my code: it creates a DataTable, but doesn't populate any of the data to it.

    function loadData() { //Initializing the AJAX Request function to load in the external list data from different subsites
        //create an array of urls to run through the ajax request instead of having to do multiple AJAX Requests
        var urls = ["url1","url2","url3"];
            
        for (i=0; i < urls.length; i++) { //for loop to run through the AJAX until all URLs have been reached
          $.ajax({
            url: urls[i],
            'headers': { 'Accept': 'application/json;odata=nometadata' },
            success: function (data) { // success function which will then execute "GETTING" the data to post it to a object array (data.value)
              data = data;
              var table = $('#myTable').DataTable();
              table.rows.add( data.value ).draw();
            }
          });
        } // missing bracket
    }
    function format ( data ) {
        // `d` is the original data object for the row
        return '<table cellpadding="5" cellspacing="0" border="0" style="padding-left:50px;">'+
            '<tr>'+
                '<td>Program:</td>'+
                '<td>'+data.Program+'</td>'+
            '</tr>'+
            '<tr>'+
                '<td>Recipient:</td>'+
                '<td>'+data.To+'</td>'+
            '</tr>'+
            '<tr>'+
                '<td>Date:</td>'+
                '<td>'+data.Date+'</td>'+
            '</tr>'+
            '<tr>'+
                '<td>Approved:</td>'+
                '<td>'+data.Approved+'</td>'+
            '</tr>'+
            '<tr>'+
                '<td>Additional Notes:</td>'+
                '<td>'+data.Notes+'</td>'+
            '</tr>'+
        '</table>';
    }
    $(document).ready(function() {
        var table = $('#myTable').DataTable( {
            "columns": [
                {
                    "className":      'details-control',
                    "orderable":      false,
                    "data":           null,
                    "defaultContent": ''
                },
                { "data": "Deliverable" }
            ],
            "order": [[1, 'asc']]
        } );
loadData();
          
        $('#myTable tbody').on('click', 'td.details-control', function () {
            var tr = $(this).closest('tr');
            var row = table.row( tr );
      
            if ( row.child.isShown() ) {
                // This row is already open - close it
                row.child.hide();
                tr.removeClass('shown');
            }
            else {
                row.child( format(row.data()) ).show();
                tr.addClass('shown');
            }
        } );
    } );
BeerusDev
  • 1,444
  • 2
  • 11
  • 30
  • I think the idea from [this answer](https://stackoverflow.com/a/63600858/12567365) was for you to gather all the data from your 3 source URLs into a single JavaScript object - and then to use that data to populate your DataTable. – andrewJames Aug 28 '20 at 16:43
  • The link in your question is to the DataTables home page. Did you mean you want to implement [this example](https://datatables.net/examples/api/row_details.html)? – andrewJames Aug 28 '20 at 16:44
  • @andrewjames yes, sorry for the wrong hyper link – BeerusDev Aug 28 '20 at 17:12
  • @andrewjames check my most recent edit, I followed that but I can't get the data to populate? – BeerusDev Aug 28 '20 at 17:15
  • OK - understood. I have proposed an approach which may help. – andrewJames Aug 28 '20 at 20:56

1 Answers1

1

Here is an approach which uses the DataTables child rows approach, combined with your approach which needs to merge data from three different URLs.

I have modified the logic which handles the three URLs so that all the data is fetched and combined before the DataTable is initialized.

This uses test data at a real URL: https://jsonplaceholder.typicode.com/posts. In my case, I use this same URL three times - each time it fetches the same 100 records.

The end result looks like this:

enter image description here

Here is the full example:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Demo</title>
  <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
  <script src="https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js"></script>
  <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.10.21/css/jquery.dataTables.min.css">
  <link rel="stylesheet" type="text/css" href="https://datatables.net/media/css/site-examples.css">

  <style>
  td.details-control {
    background: url('details_open.png') no-repeat center center;
    cursor: pointer;
  }
  tr.shown td.details-control {
    background: url('details_close.png') no-repeat center center;
  }
  </style>
</head>

<body>

<div style="margin: 20px;">

    <table id="example" class="display dataTable cell-border" style="width:100%"></table>

</div>

<script type="text/javascript">

getTableData();

function initializeTable(dataSet) {
  $(document).ready(function() {

    var table = $('#example').DataTable( {
      data: dataSet,
      columns: [
        {
          className: 'details-control',
          orderable: false,
          data: null,
          defaultContent: ''
        },
        { title: 'User ID', 
          data: 'userId' },
        { title: 'ID', 
          data: 'id' },
        { title: 'Title',
          data: 'title' }
      ]
    } );

    // event listener for opening and closing child rows:
    $('#example tbody').on('click', 'td.details-control', function () {
      var tr = $(this).closest('tr');
      var row = table.row( tr );
 
      if ( row.child.isShown() ) {
        // This row is already open - close it
        row.child.hide();
        tr.removeClass('shown');
      }
      else {
        // Open this row
        row.child( format(row.data()) ).show();
        tr.addClass('shown');
      }
    } );

  } );
}

async function getTableData() {

  let dataSet = [];

  var urls = [ 'https://jsonplaceholder.typicode.com/posts', 
               'https://jsonplaceholder.typicode.com/posts', 
               'https://jsonplaceholder.typicode.com/posts' ];

  for (var i = 0; i < urls.length; i++) {
    try {

      let response = await fetchUrl(urls[i]);

      // combine each response into one variable:
      if (Array.isArray(response)) {
        dataSet = [...dataSet, ...response];
      } else { // assume it's an object {...}
        dataSet.push(response);
      }

    } catch(err) {
      console.log(err);
    }
  }

  //console.log(dataSet);
  initializeTable(dataSet);

}

function fetchUrl(url) { 
  return $.ajax( {
    url: url,
    type: 'GET'
  } );
};

function format ( d ) {
  // `d` is the original data object for the row
  return '<table cellpadding="5" cellspacing="0" border="0" style="padding-left:50px;">' +
    '<tr>' +
      '<td>Title:</td>' +
      '<td>' + d.title + '</td>' +
    '</tr>' +
    '<tr>' +
      '<td>Body:</td>' +
      '<td>' + d.body + '</td>' +
    '</tr>' +
    '<tr>' +
      '<td>Extra info:</td>' +
      '<td>And any further details here (images etc)...</td>' +
    '</tr>' +
  '</table>';
}

</script>

</body>
</html>

I also downloaded the two button image files details_open.png and details_close.png from the DataTables web site.

I don't know why you are not seeing any of your data displayed in the table, but in my case, I force the DataTables initialization step to wait until all data has been fetched before I perform the table initialization. That may be one reason why you do not see any data.


Update

The JSON response looks like this:

{
    "value": [{
        "Notes": "Example Notes\n",
        "Approved": "Yes",
        "Program": "AMMO",
        "Date": "12/23/2018",
        "To": "example@example.com",
        "Deliverable": "Monthly Status Report (MSR)"
    }, {
        "Notes": "Example Notes\n",
        "Approved": "Yes",
        "Program": "AMMO",
        "Date": "03/30/2020",
        "To": "example@example.com",
        "Deliverable": "Meeting Minutes"
    }, {
        "Notes": "Example Notes\n",
        "Approved": "Yes",
        "Program": "AMMO",
        "Date": "12/23/2018",
        "To": "example@example.com",
        "Deliverable": "Monthly Status Report (MSR)"
    }, {
        "Notes": "Example Notes\n",
        "Approved": "Yes",
        "Program": "AMMO",
        "Date": "03/30/2020",
        "To": "example@example.com",
        "Deliverable": "Meeting Minutes"
    }, {
        "Notes": "Example Notes",
        "Approved": "No",
        "Program": "AMMO",
        "Date": "09/22/2022",
        "To": "example@example.com",
        "Deliverable": "Monthly Status Report (MSR)"
    }]
}

I will assume this is the structure of the response from all 3 of the URLs you want to use.

Because each response is nested in an object { "value": [...] }, we have to do some extra work to access the data correctly.

Changes needed from the approach shown above:

      // combine each response into one variable:
      if (Array.isArray(response)) {
        dataSet = [...dataSet, ...response];
      } else { // assume it's an object {...}
        dataSet.push(...response.value);
      }

In the above section of the code, I changed one line from this:

dataSet.push(response);

to this:

dataSet.push(...response.value);

This does 2 extra steps:

(1) It access the value array in each response.

(2) It flattens that array into each separate item in the array, so that they can be combined into a new array (which will include data from all 3 URLs).


July 27 Update

Not a solution, just some notes relating to your latest comments and the updateOutsideCount function:

  1. The function uses an if statement here:
if (moment($('#dpicker').val()).isBetween(searchMon, searchFri)){

I am not sure what the purpose of this statement is. See also points (2) and (3).

  1. Be aware than when you use isBetween() you need to be careful. The match is "exclusive". This means that if you have a date of 07/19/2021 and you want to check if this date is between 07/19/2021 and some later date, then this will fail.

isBetween() means "later than the first date" and "before the second date".

It does not mean "later than or equal to the first date".

You can change this behavior - see below.

  1. Instead of the if statement mentioned in (1) (or maybe as well as that statement), you need an if statement for your flattened map:
transformedResults.flatMap(t=>t.Days)

The above statement generates a list of every date in your data set. So you need to throw away those dates which are outside the selected week:

transformedResults.flatMap(t=>t.Days).forEach((dayArray) => {
  if ( moment(dayArray.Date).isBetween(searchMon, searchFri, undefined, '[)') ) {
    switch(dayArray.Day) { ... }
  }
}

Note that the isBetween function now includes [) as a parameter. The [ means the first date is inclusive and the ) means the second date is exclusive.

Without this [ we would fail to pick up the Monday date you want.

  1. Within your updateOutsideCount function you can declare this:
var totalOutsideCount = {P:0,TW:0,TRV:0,NR:0,PTO:0,H:0};

And then in the forEach((dayArray) loop you can increment the relevant values:

totalOutsideCount[dayArray.Status]++;

That gives you the total counts you need.

Altogether, my version of your updateOutsideCount function is as follows:

function updateOutsideCount() {
  console.clear();
  var totalOutsideCount = {P:0,TW:0,TRV:0,NR:0,PTO:0,H:0};
  moment(moment($('#dpicker').val()));
  if (moment($('#dpicker').val()).isBetween(searchMon, searchFri)) {
    transformedResults.flatMap(t => t.Days).forEach((dayArray) => {

      if (moment(dayArray.Date).isBetween(searchMon, searchFri, undefined, '[)')) {

        switch (dayArray.Day) {
          case 'Monday':
            console.log(dayArray.Status);
            break;
          case 'Tuesday':
            console.log(dayArray.Status);
            break;
          case 'Wednesday':
            console.log(dayArray.Status);
            break;
          case 'Thursday':
            console.log(dayArray.Status);
            break;
          case 'Friday':
            console.log(dayArray.Status);
            break;
        }
        totalOutsideCount[dayArray.Status]++;

      }

    });
    console.log(totalOutsideCount);
  }
}

When I select a date of 07/19/2021 this prints the following to the console:

Console was cleared. 
P
P
P
NR
P
Object { P: 4, TW: 0, TRV: 0, NR: 1, PTO: 0, H: 0 }

That is probably all I can do to help you with this. Good luck! I think you already have the code you need for updating the HTML (and clearing it).

andrewJames
  • 19,570
  • 8
  • 19
  • 51
  • So I tried it out, and the datatable doesn't populate. I look in the console and the data object doesn't show up in the network response. – BeerusDev Aug 31 '20 at 12:24
  • Also, so in my DataTable if I want there to only be two main rows, based on the "Deliverables" column. The two options are Monthly Status Report and Meeting Minutes, and all of the corresponding deliverables go under the same row. – BeerusDev Aug 31 '20 at 12:47
  • _the data object doesn't show up_ - Do you mean you are not getting any response from my example URL (https://jsonplaceholder.typicode.com/posts), or are you getting no response from your URLs (`["url1","url2","url3"]`)? – andrewJames Aug 31 '20 at 13:34
  • Can you provide the updated version of the code you are using (add it to your question, or, even better, [create a fiddle](https://jsfiddle.net/), if possible)? That will help troubleshooting - thank you. – andrewJames Aug 31 '20 at 13:35
  • Sorry, I didn't see your comment two up. I am not getting a response from my URLS, I use the correct sharepoint urls, and nothing happens. – BeerusDev Aug 31 '20 at 14:06
  • Thank you for posting the fiddle. It doesn't follow the approach I provided in my answer. For example, in my approach, I start with a call to `getTableData();` - and that handles all the data retrieval from the 3 URLs. The very last step, at the end of data retrieval is a call to `initializeTable(dataSet);`. That function is where the DataTable is initialized. In other words, the DataTable is not initialized until _after_ we have finished fetching all the data. You will see that even the `$(document).ready()` function is not called until after all the data has been fetched. – andrewJames Aug 31 '20 at 14:10
  • My suggestion: Create a new file containing exactly my approach, and make sure it works (using my URLs). Then replace my URLs with the ones you need, and my column names with your column names. After that is still working, then you can add in the remaining functionality (e.g. for buttons, etc.). – andrewJames Aug 31 '20 at 14:12
  • https://jsfiddle.net/zafktpj5/ I tried the exact same like you said and nothing happens no table forms – BeerusDev Aug 31 '20 at 14:28
  • No data is fetched from your URLs? Or, data is fetched but not displayed? If data is fetched, then can you show a sample of the JSON, so we can see the JSON's structure? – andrewJames Aug 31 '20 at 14:35
  • It doesn't fetch any data, I have checked the Network (XHR) tab in the console doesn't return any data – BeerusDev Aug 31 '20 at 14:37
  • Uncaught SyntaxError: Unexpected identifier IT is saying my URL/at least the first one has a syntax error, but it is correct? – BeerusDev Aug 31 '20 at 14:46
  • OK thanks. I don't think I can help with that - I don't know what your URLs are for your site, or what the syntax error is that you are seeing. – andrewJames Aug 31 '20 at 14:52
  • DataTables warning: table id=example - Requested unknown parameter 'Deliverable' for row 0, column 1. For more information about this error, please see http://datatables.net/tn/4 – BeerusDev Aug 31 '20 at 14:54
  • OK - that means you _are_ getting a data response from the URL. This comes back to my question: What is the JSON structure of a response? It does not contain a field called `Deliverable` - which you told it to expect: `data: 'Deliverable'`. Maybe the field is called `deliverable`? But we need to see the JSON to know for sure. – andrewJames Aug 31 '20 at 14:58
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/220745/discussion-between-andrewjames-and-zgoforth). – andrewJames Aug 31 '20 at 15:01
  • is there anyway I can create a new room with you? Im having an issue with my datatable/js of the last piece of functionality of my table, but I cannot post -.- – BeerusDev Jul 27 '21 at 15:38
  • 1
    Happy to help within the limitation of comments (but not in a room). It sounds like you probably need to create a new question with a MRE - that way you can get help from the entire community. – andrewJames Jul 27 '21 at 18:28
  • Thanks for the reply! I have created a MRE, but "Have reached my question limit" after I got downvote bombed :(. https://jsfiddle.net/BeerusDev/gm10rqp7/543/ Everything inside of my footerCallback is working except my updateOutsideCount function. – BeerusDev Jul 27 '21 at 18:32
  • 2/2 I am trying to create a function so when the user searches outside of the current week, and it shows the items only in the week they are looking for. I need to create a count of all of the statuses for every item in each day column of that week like is displayed here in the current week jsfiddle.net/BeerusDev/… I am getting the Days and the Status using flatMap() and map() and all I really have left to do is condition "if these days with the corresponding status" are in between the monday and friday of the isoWeek that was searched, then create a count for each of those days – BeerusDev Jul 27 '21 at 18:32
  • **UPDATE**: I am finally able able to get the Day and Status by day using a switch statement. I just need to create a count now, but I can't do that because it is not 1. console.clear()ing, and 2. When I tried another method it adds the count to the already current week count so I need to figure out how to overwrite the HTML when the datepicker is used, then return to normal after you clear the date picker (which the table already returns to normal on clear) https://jsfiddle.net/BeerusDev/gm10rqp7/562/ – BeerusDev Jul 27 '21 at 19:42
  • 1
    Sorry to hear about the voting troubles. It happens, sometimes. Your DataTable logic is complex, and it would take time to review. I will look at it if/when I have some time, but I have no idea when that may be. – andrewJames Jul 27 '21 at 20:12
  • Thank you Andrew. I really appreciate it. I feel like I am so close but I am experiencing what seems like coder's block. – BeerusDev Jul 27 '21 at 20:17
  • 1
    OK - some notes added. Hope they help - but I think you should try to figure out the rest for yourself (or wait until you can ask questions again). All the best! – andrewJames Jul 27 '21 at 23:12
  • ***facepalm***, IF you have time, for some reason it works perfectly in the fiddle with the static data https://jsfiddle.net/BeerusDev/3yew9kcd/36/ (see here). But when I convert it to my dynamic version with AJAX, I get the following error in the new function `transformedResults is not defined`. I change transformedResults to `data.Days.flatMap();` and then I get the following error: `Cannot read property 'Date' of undefined` @ line 442. Here is dynamic version JS https://jsfiddle.net/BeerusDev/053zyoL4/2/ – BeerusDev Jul 28 '21 at 14:39
  • I cannot figure out why dayArray is defined in one example. And then in the other with the same exact function, the dayArray is undefined. Any clues? I have been debugging for quite a bit haha and nothing is happening – BeerusDev Jul 28 '21 at 20:27
  • 1
    I don't know - but bear in mind that your jQuery ajax call (started by `loadData()`) is asynchronous. The DataTable initialization is also asynchronous. Neither one will wait for the other. You have to ensure you have finished fetching your data first before you try to initialize your table. There are lots of questions about this async poblem - here is [one](https://stackoverflow.com/questions/21052258/passing-data-outside-ajax-call-jquery). I cannot run your ajax-based Fiddle, so this is a complete guess on my part. – andrewJames Jul 28 '21 at 21:41
  • 1
    Most of the time, you avoid this async data fetching problem by placing your ajax call inside the DataTables definition - then DataTables takes care of things for you. But you don't have to do that, as long as you work around the async problem as shown in various SO answers. – andrewJames Jul 28 '21 at 21:41
  • 1
    Andrew, thought I would share with you I figured it out. In my dynamic version, I don't need a `flatMap()`. My arrow function now looks like `data.Days.forEach((dayArray => {...}` which contains the conditionals and the switch – BeerusDev Jul 29 '21 at 18:11
  • Well, I thought I did. Here is the working example (JS sourced data, watch the console when you search back to last week its perfect) https://jsfiddle.net/BeerusDev/1dshjpm3/153/ . Here on the other hand, is my entire script for the dynamic application. The only small differences is the updateOutsideCount function (the forEach) https://jsfiddle.net/BeerusDev/rvhedj0s/2/ . If you have a chance, can you help me understand why in the second one my count for each day keeps resetting? – BeerusDev Aug 03 '21 at 15:43
  • if you get a moment, can you take a look at this custom filter issue https://sharepoint.stackexchange.com/questions/297364/datatables-custom-filter-not-accepting-sharepoint-ifuseringroup – BeerusDev Sep 09 '21 at 13:46
  • You would need to provide data. For example, if `tableUserTitle === thisUserTitle`, then that will evaluate to true - and therefore `if (isCurrentUserInGroup)` will never be evaluated. What happens if you swap the order of those 2 `if` statements? – andrewJames Sep 09 '21 at 14:29
  • I tried the following, and have been trying to debug, ` if(tableUserTitle != thisUserTitle && isCurrentUserInGroup){ return true; } else if (tableUserTitle == thisUserTitle) { //if the current user equals the same user in the task list row, then it only will show that users items to them so they cannot see others tasks return true; }` and I still can't get it to work. I am trying to find a similar way to get thisUserTitle with something like thisUserGroup so I can use && to have a multi condition – BeerusDev Sep 09 '21 at 14:40