1

Application is using:

  • DataTables 1.10.18
  • jquery 3.2.1
  • PHP back-end
  • lodash 4.17.4

The application contains a web page which consists of multiple DataTables. Each of these uses serverSide: true (server-side mode) to obtain the data via an ajax endpoint which returns JSON data.

The tables are initialised as follows:

  1. On page load several <table>'s are rendered. I'm using a jquery .each() to initialise the DataTable for each one:

    $.each($('table'), function () {
    
        $(this).DataTable({
            processing: true,
            serverSide: true,
            searching: false,
            ajax: {
                data: {
                   table_id: $(this).attr('id') 
            },
            url: '/get-data.json', 
        },
        ...
    });
    
  2. Each <table> has an ID. This is passed via ajax in the data: attribute. The endpoint /get-data.json returns data based on the table ID. In other words it knows "which table" the data should be obtained for based on this ID.

I want to be able to do searching on tables, but it must be done server-side. For this reason my initialisation code in (1) sets searching: false because this effectively disables the client-side search facility that DataTables provides (which we can't use in this instance as searching must be done server-side).

The problem I'm facing is how to create search inputs for each table, make an ajax call and update the appropriate table. I want the search to work in realtime after >=3 characters have been entered. Critical to this question is that 1 search input is responsible for searching 1 DataTable - it's not a search feature where the input can update "any/every table on the page" which is a commonly described pattern in other questions. 1 input : searching 1 table in this case.

My plan has been as follows - each table referenced in point (2) has an ID. I need to create unique inputs. So if I have tables with ID's #table1, #table2, #table3 I can easily create:

<input type="text" name="table1_search" id="table1_search"> 
<input type="text" name="table2_search" id="table2_search"> 
<input type="text" name="table3_search" id="table3_search"> 

I then detect if any changes have occurred on inputs:

$('input[type="text"]').bind("keyup change input",
        function (e) {
            // Ignore tab key for keyup event otherwise it'll fire an ajax request that does nothing useful.
            if (e.which !== 9) {
                processSearch.call(this);
            } else {
                e.preventDefault();
            }
        });

    var prev_value = {};
    function processSearch() {
        var obj = $(this),
                search_id = obj.attr('id'), // ID of input
                search_value = obj.val(); // Value of input

        // There's been no change to the field, ignore.
        if (prev_value[search_id] === search_value) {
            return;
        }

        prev_value[search_id] = search_value;

        /* Wait until at least 3 characters have been entered, or user has cleared the input */
        if (search_value.length >= 3 || (!search_value)) {
            debouncedDraw({search_id: search_id, search_value: search_value});
        }
    }

The above code does what I need in terms of waiting for >=3 characters to be entered. I'm then executing a function called debouncedDraw which passes an object containing search_id and search_value. These refer to the input ID and value respectively, e.g. if I type "foo" into #table1_search then the object is:

{search_id: 'table1_search', search_value: 'foo'}

The debouncedDraw function looks like this. This is using lodash to limit the rate at which the function can fire. The point here is to stop it making needless ajax requests based on a question I asked a few years ago here: DataTables - kill ajax requests when a new one has started:

var debouncedDraw = _.debounce(function (opts) {
    console.log(opts);
}, 500);

At the moment this will just console.log the object given above.

I'm unsure of the best way to proceed at this point. I need to re-run /get-data.json via ajax and then update the appropriate table.

I could access the request data and split the search_id based on the underscore to work out which table ID the data is for (e.g. table1_search targets #table1). I then need to write this data back to the appropriate table (#table1 in this case).

I can't help but think I'm going about this in a convoluted way and wondered if DataTables itself has any better ways of supporting this? It seems quite a basic requirement (multiple searchable tables in serverSide mode). But I can't find any posts which refer how to do this specifically.

halfer
  • 19,824
  • 17
  • 99
  • 186
Andy
  • 5,142
  • 11
  • 58
  • 131

2 Answers2

1

All the "gotchas" I've experienced over the years is encapsulated in the snippet below. This is the basic template I always use when creating a new datatable. You can create as many datatables on a page as you need using this pattern.

Personally I would use a different ajax url path/route for each table so that table logic is in separate files in the backend... but it is possible to have all the data logic in a single backend file. I modified my usual template to suit that.


<script> //I usually put the script section in the head tag

var table_1; //declare your table var here and initialize as a datatable inside document ready below.

$(document).ready(function() {

    table_1 = $('#table_1').DataTable( {
        dom: "Bfrtip",
        ajax: {
            url: "/get-data.json?table=table_1",  //add query string var for backend routing
            type: "POST"  //use POST to not have to deal with url encoding various characters
        },      
        serverSide: true,
        searchDelay: 2000,  // use this instead of custom debounce
        processing: true, // optional visual indicator that a search has been sent to backend
        lengthMenu: [ 10, 25, 50, 75, 100 ], // define per page limits. first value will be the default
        buttons: [
            "pageLength" // per page drop down button. i usually override/extend the default button
        ],      
        columns: [ // column definitions of json data fields
            { data: "col_1", title: "ID", width: "1%" },  // width: 1% makes col width as small as possible
            { data: "col_2", title: "Label 2", visible:false }, //visible: false allows you access to field data without displaying to user
            { data: "col_3", title: "Label 3", render: function ( data, type, row ) { //render allows combining of fields into single column
                    return data + ' <small>('+row.col_2+')</small>'; // data will be col_3 value. row.col_2 is how you reference col_2 value
            } },
            { data: "col_4", title: "Label 4", searchable:false }, //searchable: false set this field to not be used in search
        ],
        rowId: 'col_1' //sets the tr row id to the value in this column. useful for DOM and other manipulation later
    } );
}

</script>

<table id="table_1" class="table table-striped table-bordered table-sm" style="width:100%"></table>

<!-- If you define title attributes in col definitions above you don't need to create html table headers/footers. Just an empty table tag will do. -->

With this pattern you can utilize the built-in search input that comes with datatables for your use case with server-side processing on all tables.

There's a method behind my madness which I tried to document in the script comments on each line. Let me know if you have a question on something. I'm thinking this is bounty worthy.

For reference, when developing a new app using datatables I basically live on this page https://datatables.net/reference/option/

Edit 1

Inside your existing debounced drawTable function you could do something like this:

function drawTable(id) {
    $('#'+id).DataTable().ajax.url( 'get-data.json?table_id='+id+'&foo=bar' ); //update ajax url of existing dt - if necessary
    $('#'+id).DataTable().search(search_input_val).draw(); // fire ajax request with value from your custom search input

}

I'm fairly certain you will need to set "searching" to true though for this method to work.

Edit 2

Another way I just thought of, without using dt search. Pass all your data through modified url and load/reload.

$('#'+id).DataTable().ajax.url( 'get-data.json?table_id='+id+'&search=foo' ).load();

You could then get rid of all the debounce stuff if you use a button click listener or an onblur listener on the input field and fire the same command above.

Have you seen this? https://datatables.net/reference/api/%24.fn.dataTable.util.throttle()

I've never used it before, but it looks like a debounce. The example on the page shows it being used for .search()

Community
  • 1
  • 1
Aaron
  • 1,093
  • 8
  • 13
  • I've tried this and unfortunately it's a worse solution that what I had. The reason we used custom inputs and did the debounce was to ensure it isn't making pointless ajax requests - see the linked SO post in my question. Even if you use `searchDelay` then type keywords it's making multiple ajax requests (one initially, followed by a delayed one later). The code I had mitigates against this, as well as ensuring >=3 characters have been entered before performing the search. I don't think there's anything else in the code given which improves it? – Andy Sep 23 '19 at 07:50
  • I see what you mean, is the multi ajax request on typing your main issue? Or is it the "destroying" aspect of your current solution? – Aaron Sep 23 '19 at 08:06
  • It's everything mentioned in the question ;) There are multiple challenges with it. The linked SO post is really important as that's quite key to performance, especially when there are multiple tables. Destroying/recreating seems to be a performance bottleneck - and is also mentioned in the DataTables documents as undesirable. So that's something I don't have a solution to right now. – Andy Sep 23 '19 at 08:09
  • Ok, why don't you just use the .search() API inside of your debounce function? Trigger a dt search instead of a destroy. – Aaron Sep 23 '19 at 08:15
  • Well, that's what the question is asking. If you have code that can demonstrate how to do that I'd be keen to see it. – Andy Sep 23 '19 at 08:18
  • It's a bit confusing jumping back and forth from your original Q and A, but I've updated my answer with what I think will work for your case. – Aaron Sep 23 '19 at 08:55
  • Any luck? You could put Edit 2 inside your existing drawTable function as well – Aaron Sep 25 '19 at 07:10
  • I've awarded the bounty for the effort you've put into the answer. Haven't actually used it - I've kept what I already had. The docs on throttle you linked to actually says it's not a debounce function. Manually initialising tables (ref `table_1`) is more convoluted that the `.each()` I had. I can't comment on Edit 2 as I haven't figured out how to implement that. Thanks for the effort you put into the answer I'm sure it will be of use to someone in future. The conclusion I came to is that I was wondering if it was a convoluted problem...and the answer is clearly yes, it's a pain to implement! – Andy Sep 25 '19 at 10:45
  • Thanks, I really appreciate that! It's too bad there isn't a direct message feature, I wouldn't mind taking another swing at your project. Last I read on dt forums there are some improvements around this topic coming in v2.0.... – Aaron Sep 25 '19 at 23:46
0

I've implemented the following but would prefer a better solution as I don't think this is efficient and it definitely isn't elegant!

Taking the code from the question I modified the debounce function as follows:

var debouncedDraw = _.debounce(function (opts) {
   // Destroy the existing DataTable. 
   $('#' + opts.search_region).DataTable().destroy(); 

   // Re-run the drawTable method to get the new DataTable with the search results
   drawTable(opts.search_region);
}, 500);

I introduced a function called drawTable which takes the ID of a <table> and runs the DataTables initialisation code. The ajax object was also modified to take into account anything entered into the search keywords input for the given table ID:

function drawTable(id) {
    $id = $('#'+id); // Convert string ID to jquery identifier

    $id.DataTable({
        // DataTable initialisation code as per question
        ajax: {
            data: {
               table_id: id,  
               keywords: $('input[name="keywords_' + id + '"]').val() // search keywords for table_id
            },
            url: '/get-data.json', 
        },
        // ... code as per question
    });
}

The $.each() was modified so that it detects the ID of each <table> on page load and calls drawTable:

$.each($('table'), function () {
    drawTable($(this).attr('id'));
});

This "works" in that it creates each of the required DataTable's on page load, and also handles the search. The search input names were modified to the format: keywords_ plus the ID of the table, e.g. keywords_table1.

I don't think this is efficient because I'm having to call destroy on my DataTable. As per the docs:

This has a very significant performance hit on the page, since a lot of calculations and DOM manipulation is involved, so if you can avoid this, and use the API, that is very strongly encouraged!

However the reason I'm doing this is also as given in the same docs:

DataTables does not allow initialisation options to be altered at any time other than at initialisation time. Any manipulation of the table after initialisation must be done through the API

Well, I'm not using the client-side search feature as I'm having to do searching server-side. So I'm unsure whether manipulating the table via the API would actually help in this instance anyway.

Are there better ways of achieving this?

Andy
  • 5,142
  • 11
  • 58
  • 131
  • For clarification, are you using the Datatables "Editor" for PHP in you backend? https://editor.datatables.net/ I've made several datatables heavy apps. Just made one this week with 8 on the page all server side. Editor makes things a ton easier. – Aaron Sep 22 '19 at 06:20
  • Also setting "searching" to false because you want to achieve server side search does not make sense. This is forcing you to reinvent the wheel with those multiple hand-coded input fields you mentioned. You can have searching:true AND serverSide:true. – Aaron Sep 22 '19 at 19:27
  • @Aaron no I'm not using the Editor because this particular set of tables is only displaying data. There is no functionality to edit it in this part of the app. – Andy Sep 23 '19 at 07:38
  • Gotcha, the reason I ask is because it comes with backend libraries in multiple languages. I use those libraries in "display only" datatables as well. – Aaron Sep 23 '19 at 07:42