Multiple tables with individual search inputs in serverSide mode using DataTables - javascript

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:
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',
},
...
});
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.

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()

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?

Related

How do you programatically open multiple rows in DataTables

I thought this question would have been answered but I can't work this out. Have tried:
https://datatables.net/forums/discussion/25833/is-there-any-way-to-programmatically-select-rows
https://datatables.net/reference/api/row().select()
I'm using DataTables 1.10.16 in serverSide mode - my data is loaded in via ajax as opposed to being there on page load.
My markup is simply a table with an ID, #substancesTable:
<table id="substancesTable" cellspacing="0" width="100%">
<thead>
<tr>
<th>ID</th>
<th>EC</th>
<th>CAS</th>
<th>Name</th>
</tr>
</thead>
</table>
The js to load the data is as follows:
var substancesTable = $('#substancesTable').DataTable({
"processing": true,
"serverSide": true,
"searching": false,
"ajax": {
"url": "/get-substances.json",
"dataSrc": function (json) {
return json.data;
}
}
});
This populates my table fine. I have an event handler such that when a user manually clicks on on a row (any <td> element inside the #substancesTable) it makes a further ajax request to obtain more data which is then populated inside the <td> that the user clicked. This code is also responsible for closing/collapsing any open rows:
$('#substancesTable tbody').on('click', 'td', function () {
var tr = $(this).closest('tr');
var row = substancesTable.row( tr );
if ( row.child.isShown() ) {
row.child.hide();
tr.removeClass('shown');
}
else {
row.child( expand_substance(row.data()) ).show();
tr.addClass('shown');
}
} );
The code above calls a function expand_substance which handles the ajax request mentioned. This all works fine.
What I'm trying to do is find a way to programatically open certain rows. What I mean by this is having an array of row ID's that the user has clicked on, e.g.
var openRows = [5, 6, 8, 33, 100];
This array data will be stored in Redis (cache) so if the user navigates away from the page, when they return, the data in openRows will be loaded and I want to open the appropriate rows. But I don't know how to tell DataTables to open rows 5, 6, 8, 33, 100, etc.
The links above don't seem to work for me. For example, if I try:
substancesTable.row(':eq(0)', { page: 'current' }).select();
I get a console error:
VM308:1 Uncaught TypeError: substancesTable.row is not a function
I'm not sure if that's even how to open the row but couldn't find any more information that helped.
So, is it possible to use JavaScript to open certain rows of the table based on an array of known ID's (openRows)?
That one was fun to resolve (hoping I did)... since it is quite complicated and tricky.
First, I have to mention that it's not possible (or at least a pain) to build a demo using the server side feature, so I used DataTable's "zero configuration" example.
Now, I hope I correctly understand that a row index array is to be previously stored from user row clicks... And that it's the starting point of the current question to reuse that array to manipulate the rows.
In my example, there are only 57 rows... So I used this array: var targets = [5, 6, 8, 33].
The solution step by step:
Use the drawCallback to run a for loop on the array.
Get the drawn rows in the right order... Which means after sort.
We need to use the row-selector along with the useful { order: 'applied' } trick.
(Found in a dataTables forum question)
Get the nodes from it.
Target the right rows, based on the array, using the jQuery .eq() method.
So we have to create a jQuery object with the row collection first (wrap with $()).
Manipulate the rows!
In my example, I just added a red background color to the child td.
You will do your things from here.
So here is the whole function:
"drawCallback": function(){
var api = this.api();
for(i=0;i<targets.length;i++){
$(api.rows({ order: 'applied' }).nodes()).eq(targets[i]).find("td").addClass("red");
console.log(targets[i]);
}
}
CodePen
Remember that rows are zero-based...
Notice that the row indexes manipulated above are after sort (so it reflects the order as currently displayed to user, not the order as supplied to Datatables from the markup or from Ajax.) That means the sorting shall be the same as when the user clicked the rows (thus saved the indexes). That may be a problem... ;)
The answer to this was provided by a colleague and makes use of the rowCallback (https://datatables.net/reference/option/rowCallback) callback which DataTables provides.
var substancesTable = $('#substancesTable').DataTable({
// ...
"rowCallback": function(row) {
var id = $(row).find('td:first').text();
var index = $.inArray(id, openRows);
if (index !== -1) {
var tr = $(row).closest('tr');
var row = substancesTable.row( tr );
row.child( expand_substance(row.data()) ).show();
tr.addClass('active');
}
}
});
This callback will post process each row (represented by row). The line var index = $.inArray(id, openRows); means is the current row (identified by the text from var id) in the array of openRows. Since the first column in my table contains the ID that's how we can get a match between var id and openRows.
If the ID is found it will then trigger a function I've written called expand_substance(). This is nothing to do with DataTables, it's a custom js function.
In my case the expand_substance() function is responsible for doing an ajax call to obtain some more details which are then populated into the row:
function expand_substance ( rowData ) {
var div = $('<div/>')
.text('Loading...');
$.ajax( {
url: '/view-substance/expand-substance/' + rowData.id,
dataType: 'html',
success: function ( data ) {
div.html(data);
}
});
return div;
}
This is only required because when the user expands a row in my application the details which are shown are obtained via an ajax request. In theory expand_substance() might not be used if the data was there on page load.
This works in my application. The other Answer provided to this post is along the right lines but does not use ajax source data, and it does not expand a row, but instead just highlights it in red. Therefore I've provided my own answer because this fully addresses the question and others may find it useful.

Use the same data for multiple tables with tabulator

I'm using jQuery and tabulator to create a fairly simple webpage showing the progress of various live events. I have three table elements on the page showing events in different states. Broadly speaking I have a table for events that are 'Live', 'Finished' and 'Upcoming'.
The tabulator code for each table looks a bit like this:
var eventTable = {
layout:"fitColumns",
columns:[
{title:"Event ID", field:"eventID"},
{title:"Event name", field:"eventName"},
{title:"Event status", field:"eventStatus"}
]
};
I can then generate and populate the tables using data retrieved from the server using an AJAX call:
$('#live-events-table').tabulator(eventTable);
$('#live-events-table').tabulator("setData", "/cgi-bin/getevents.py");
$('#live-events-table').tabulator("setFilter", "status", "in", ["intermission", "mid-event"]);
Then similar code for the finished and upcoming tables.
This creates three requests for getevents.py every time the page is updated. This is undesirable because the data is pulled from another API and I want to avoid sending multiple identical requests. Also, there's a (small) chance the data could change between requests.
Is there a way of using the same data to populate all three tables?
Learnt something today. Never heard of Tabulator. I've been using DataTables for this sort of advanced tabular layout in the form of a jQuery plugin. Tabulator looks promising. Will have to explore more in the future.
In the docs I see Tabulator - Set Table Data, lots of alternative options available to set the data.
Hope I am understanding the problem correctly.
My thoughts/flow for tacking this problem would be:
Assuming your doing this already: Prep server side (getevents.py) needs to return all 'Live', 'Finished' and 'Upcoming' rows in JSON together in 1 response. How do I make a JSON object with multiple arrays?.
Client side on doc ready(), makes a jQuery ajax call to fetch this data before 3 x tabulators are built/called.
You can use a loading indicator. So the client sees something is happening in the 3 x before replaced by jQuery tabulator once the data is returned and you built with tabulator.
In the ajax success callback function you can iterate over the returned json object (containing the 3 x eventTypes). You might have to JSON.parse() if you store it in a JavaScript variable to iterate over.
Now get your specific array of event types nested objects arrays/json.
Then call your tabulator creation methods using the setData pointing to each specific JavaScript array containing the relevant data.
Not sure if this falls under "premature optimization", but I would proceed as the problem itself is rather fun to solve, but you know the requirements/needs better than I.
Good luck.
I fixed this fairly simply, using suggestions from #RoryGS.
Define the table options in a variable, as before:
var eventTable = {
layout:"fitColumns",
columns:[
{title:"Event ID", field:"eventID"},
{title:"Event name", field:"eventName"},
{title:"Event status", field:"eventStatus"}
]
};
Then make a jQuery ajax call to fetch the data, and build the table(s) in the success option of the function:
$(function() {
$.ajax({
dataType: "json",
url: "/cgi-bin/getevents.py",
success: function(data, status){
$('#live-events-table').tabulator(eventTable);
$('#finished-events-table').tabulator(eventTable);
$('#live-events-table').tabulator("setData", data);
$('#live-events-table').tabulator("setFilter", "status", "in", ["intermission", "mid-event"]);
$('#finished-events-table').tabulator("setData", data);
$('#finished-events-table').tabulator("setFilter", "status", "in", ["post-event"]);
}
})})
It seems to be necessary to set the data individually for each table. I would like to specify the data in the constructor, but this doesn't seem to work. I will continue to try to refine this.
If you are getting the data from a URL you can pass it into the ajaxURL property in the table constructor and tabulator will make the request for you:
$("#example-table").tabulator({
ajaxURL:"/cgi-bin/getevents.pyw", //ajax URL
columns:[...] //define columns etc
});

Select2 not showing results in dropdown with AJAX

I'm working on creating an interface for some system I am working on and I am required to write the Javascript around pre-existing PHP AJAX functions that are used elsewhere in the system (purely as the person who does our DB stuff is too busy to adapt the code to my needs).
Using Select2 I need to make a select field where the user searches for their name in the database and selects the one matching it so it can then be posted to another AJAX function later. I have done this with the following code:
//<select id="sltMyName" style="width: 100%;"></select>
$("#sltMyName").select2({
ajax: {
type: "POST",
url: "includes/php/report_searchPlayers.php",
delay: 250,
data: function (params) {
return {
q: params.term // search term
};
},
processResults: function (data) {
console.log({ results: data.split(",") });
return { results: data.split(",") };
}
},
minimumInputLength: 1
});
This should turn a returned string in the format of name1,name2,name3,name... into the required format for Select2 to display, however, it does not currently work. The dropdown just appears blank.
I've seen some questions referring to a text and id attribute, however, they are all in the context of a JSON string being returned, so I am not sure if they are required here and how to utilise them in this context. Furthermore, I cannot find any of this in the documentation.
Any suggestions? Thanks in advance.
Data returned:
Tommy,Jak_Tommy_Lee_Jones,Tommy_Shelby,Tommy_Balboner,TommyCZ,GA_Tommy,VA_Tommy,Tommy_Skrattar,Tommy_Knocker,Tommy_of_Elektro,Tommy_the_Destroyer,Old_Tommy,tommy_of_house_shelby,TommyDermo,TommyC,TommyCash_CZ,Tommyb69k,SA_Tommy,tommyfaster,Tommy_See,Tommy_de_Destroyer,Tommy_of_Whiteroses,TommyShelby,Templar_Intiate_Tommy,Templar_Initiate_Tommy,tommysuckspp,Tommy_the_Overweight
I think I've figured it out. The main issue is that the data that Select2 wants to use needs to be an array of objects with at least a property named id and a property named text. In the processResults function, just create those objects & then the dropdown will populate with the data from your URL!
I've noticed that it tries to filter the data as i type, but since the url I passed in does not accept the parameters, it just returns the same data each time. Let me know if you need a more robust example.
See my fiddle at https://jsfiddle.net/yp0rp2kw/3/ for a working solution.

free-jqgrid: easier way to save, load and apply filter data including filter-toolbar text and page-settings?

I have a pretty standard jqGrid using free-jqGrid 4.13 with loadonce: true;
What I want to do is something I came across a couple of times:
I want to save the state of the grid (without the data);
that means the settings (number of current page for example), filters the user applied and also the text in the filter-toolbar.
and then load and apply this data to the grid later.
jqGrid is such a powerful tool, but stuff like this seems such a pain to accomplish.
after hours and hours of work i came up with a complicated solution; it is working, but it's not a nice solution imho:
1) saving jqGrid filter-data (locally):
// in this example i am using the "window unload" event,
// because i want the filters to be saved once you leave the page:
$(window).on("unload", function(){
// i have to access the colModel in order to get the names of my columns
// which i need to get the values of the filter-toolbar textboxes later:
var col_arr = $("#list").jqGrid("getGridParam", "colModel");
// my own array to save the toolbar data:
var toolbar_search_array = [];
for(var i = 0, max = col_arr.length; i < max; i++)
{
// only saving the data when colModel's "search" is set to true
// and value of the filterToolbar textbox got a length
// the ID of the filterToolbar text-input is "gs_" + colModel.name
if(col_arr[i].search && $("#gs_" + col_arr[i].name).val().length)
{
toolbar_search_array.push({ name: col_arr[i].name, value: $("#gs_" + col_arr[i].name).val() });
}
}
// putting everything into one object
var pref = {
// 1. toolbar filter data - used to fill out the text-inputs accordingly
toolbar : toolbar_search_array,
// 2. postData - contains data for the actual filtering
post : $("#list").jqGrid("getGridParam", "postData"),
// 3. settings - this data is also included in postData - but doesn't get applied
// when using 'setGridParam'
sortname: $('#list').jqGrid('getGridParam', 'sortname'),
sortorder: $('#list').jqGrid('getGridParam', 'sortorder'),
page: $('#list').jqGrid('getGridParam', 'page'),
rowNum: $('#list').jqGrid('getGridParam', 'rowNum')
};
//saving in localStorage
localStorage.setItem("list", JSON.stringify( pref ));
});
2) loading and applying jqGrid filter-data:
for use in a document-ready closure, for example
var localsave = JSON.parse(localStorage.getItem("list"));
// these settings are also saved in postData,
// but they don't get applied to the grid when setting the postData:
$('#list').jqGrid('setGridParam', {
sortname: localsave.sortname,
sortorder: localsave.sortorder,
page: localsave.page,
rowNum: localsave.rowNum
});
// this applies the filtering itself and reloads the grid.
// it's important that you don't forget the "search : true" part:
$("#list").jqGrid("setGridParam", {
search : true,
postData : localsave.post
}).trigger("reloadGrid");
// this is loading the text into the filterToolbar
// from the array of objects i created:
console.log(localsave.toolbar);
for(i = 0, max = localsave.toolbar.length; i < max; i++)
{
$("#gs_" + localsave.toolbar[i].name).val( localsave.toolbar[i].value );
}
it seems strange to me that postData contains all the data necessary but it's impossible to apply this data; at least as far as i know.
my question(s):
is it really this inconvenient to apply all the filter- and paging-data or am i missing something?
why can't this be as simple as:
// saving:
var my_save = $("#list").jqGrid("getGridParam", "postData");
// loading:
$("#list").jqGrid("setGridParam", {
search : true,
postData : my_save
}).trigger("reloadGrid");
?
thank you for any help and/or suggestions!
The answer with the demo provides an example of the implementation. The method refreshSerchingToolbar is relatively long. On the other side the code is still not full. It's not restore the state of operands, if the option searchOperators: true is used. I wanted to rewrite many parts of the filterToolbar method to make easier the saving/restoring of the the state of the filter toolbar or refreshing based on the changed postData. It's just the problem of the time and nothing more. What you describe is close to the feature forceClientSorting: true, which was difficult to implement in original code of jqGrid 4.7, but it was easy to implement after I rewrote large part of processing of the server response. The same problem is with the code of filterToolbar. The changes of filterToolbar is still in my TODO list and I'll made the corresponding changes soon.

Filter service in angularjs

I am rewriting an app that uses html, js and php into angular with rest php. I came across an issues regarding filters. In the actual form i have dropdowns that are populated ajax by js from php. The config in the js is made something like
form = new form('user-page');
form.ajaxselects = [ ['UserID', {table: 'users', DefaultValue: 5} ] ]
form.construct();
function form(formName) {
var this.ajaxselects;
function construct() {
foreach (this.ajaxselects as ajaxselect) {
$.post( ajaxselect[0], { Action:'get' },
function( data ) {
// exemplification, not real code
$(this).select(ajaxselect[0]).values = data;
}, 'json'
);
}
}
EDIT:
How it currently works with js:
I instantiate an object, called form and in that form i configure all my elements. Dropdowns, field validations etc.
After i configure (the configuration means that i say for a dropdown where to take the value from the server, what is the default value etc), for a validation i say what's the rule (it's mandatory, it's decimal).
And then when i run a function on the form object i go for each dropdown and populate it, add events validate the inputs etc.
I would like to know if i can and if yes, how, to build something like this in angular. To give to a service let's say a set of rules, something like:
['user_id', http_request:'server.com/api/populate/user_id', default_Value:1] and then i get the dropdown for users populated with the values. I don't know how to explain it better.

Categories

Resources