How do you programatically open multiple rows in DataTables - javascript

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.

Related

Multiple tables with individual search inputs in serverSide mode using DataTables

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?

Datatable How to make first field clickable

I am using datatable with server side processing.
Here is my data
name type cost
abc a $5
bac d $10
I want first field clickable to mydomain.com/?abc
second field to mydomain.com?bac
but data is populating with mysql so don't know how to do that
Use columns.render option to generate content for a cell.
For example:
var table = $('#example').DataTable({
columnDefs: [
{
targets: 0,
render: function(data, type, full, meta){
if(type === 'display'){
data = '' + data + '';
}
return data;
}
}
]
});
See this example for code and demonstration.
You could try building a calculated column and adding anchor to it. Below example from this reference page and this thread on calculated columns might give you an idea how to do it.
$('#example').dataTable( {
"columnDefs": [ {
"targets": 0,
"data": "download_link",
"render": function ( data, type, row, meta ) {
return 'Download';
}
} ]
} );
If you want to capture a click event and handle it in a custom way it's a little more complicated. When I needed to hack something like this together a while back I ended up having to use calculated columns to add HTML classes to cells I wanted to make interactive, wait for DataTable to be filled (listen to on load complete or on init complete, not sure now), query DOM for elements with HTML classes I used and attaching my event handlers in this manner. Main reason for this being when document initializes and attaches handlers to DOM elements, table rows are not there yet, so event's wouldn't get automagically attached.

Delete fails to remove a JSON object

I do have the contents of html tables stored in a database and I use jQuery to extract the data from it and place it in another table by creating JSON objects from these tables.
I get the stored table and append it to the DOM as follow:
$('#divfortable).append(storedtable);
The next step is to create my object :
var tableObject = $('# divfortable tbody tr').map(function(i) {
var row = {};
$(this).find('td').each(function(i) {
var rowName = columns[i];
row[rowName] = $(this).text().trim();
});
return row;
}).get();
where I have an existing array with the relevant column names.
The next step is to upload these details to the server:
data = { id: id, tableObject: tableObject };
$.ajax({
dataType: 'json',
url:'my_url',
data: $.param(data),
error: function(jqXHR, textStatus, errorThrown) { },
success: function(data) { }
});
It works fine and I am very happy with the results but my problem is when repeating the process on another table. I want to get a new table and extract its contents in a similar fashion but I cannot get rid of the previous result. The result contains the newly loaded table plus that of the previous table and if I repeat it once again, it contains the result of all 3 tables!
It is obvious to me that I must empty all objects and DOM elements from the previous extraction but I tried different ways, to no avail. I gathered from searching the internet and this forum that you can use delete or empty to destroy the tableObject so that you can create a brand new one, but it does not do the job.
I tried the following:
delete tableObject;
and
$('#divfortable).remove(storedtable);
in the success function, together and separately
I also used
$('#divfortable).remove(storedtable);
after I created my data object
data = { id: id, tableObject: tableObject };
but in this case the script does not progress to the ajax call, but by displaying $('#divfortable) it displayed both tables!
How can I get rid of the previous result?
You should be able to simply clear the current #divfortable before the second operation:
$('#divfortable').empty();

Calling fnStandingRedraw to retain the current pagination settings in Datatables using javaScriptSupport.addInitializerCall

I am using the CreateEventLink method of the ComponentResources class to create a link to a delete event on my page called UserList using the following:
resources.createEventLink("delete", user.getUserId()).toURI();
The UserList page uses datatables to create a list of user's data and a delete link. When the delete link is clicked, the delete event is called on the same page and everyone is happy.
The problem is the datatable goes back to the first page of records after a user is deleted. For example, if I am on page 5 of my datatable, on the UserList page, and I click delete it will stay on the Userlist page, but my datatable will reset itself to the first page.
After some research I haved discovered that the fnStandingRedraw plugin from datatables will fix this issue: http://datatables.net/plug-ins/api
Going through the documentation I have learned that I should use:
javaScriptSupport.addInitializerCall("fnStandingRedraw", "");
to call the following in js file via an Import notation:
Tapestry.Initializer.fnStandingRedraw = function(oSettings) {
//redraw to account for filtering and sorting
// concept here is that (for client side) there is a row got inserted at the end (for an add)
// or when a record was modified it could be in the middle of the table
// that is probably not supposed to be there - due to filtering / sorting
// so we need to re process filtering and sorting
// BUT - if it is server side - then this should be handled by the server - so skip this step
if(oSettings.oFeatures.bServerSide === false){
var before = oSettings._iDisplayStart;
oSettings.oApi._fnReDraw(oSettings);
//iDisplayStart has been reset to zero - so lets change it back
oSettings._iDisplayStart = before;
oSettings.oApi._fnCalculateEnd(oSettings);
}
//draw the 'current' page
oSettings.oApi._fnDraw(oSettings);
};
However I get the error:
Uncaught TypeError: Cannot read property 'bServerSide' of undefined
Tapestry.Initializer.fnStandingRedrawhelpdesk.js:16
$.extend.inittapestry-jquery.js:32
jQuery.extend.eachjquery-1.6.2.js:655
$.extend.inittapestry-jquery.js:26
jQuery.extend.eachjquery-1.6.2.js:649
$.extend.inittapestry-jquery.js:18
(anonymous function)list:70
jQuery.extend._Deferred.deferred.resolveWithjquery-1.6.2.js:1008
jQuery.extend.readyjquery-1.6.2.js:436
DOMContentLoadedjquery-1.6.2.js:915
Any guidance would be most appreciated...Thanks in advance!!
The best practice is to save DataTables plugin to separate js file, so you can use it not only through tapestry initializer but also from other js.:
(function($) {
$.fn.dataTableExt.oApi.fnStandingRedraw = function(oSettings) {
if(oSettings.oFeatures.bServerSide === false){
var before = oSettings._iDisplayStart;
oSettings.oApi._fnReDraw(oSettings);
// iDisplayStart has been reset to zero - so lets change it back
oSettings._iDisplayStart = before;
oSettings.oApi._fnCalculateEnd(oSettings);
}
// draw the 'current' page
oSettings.oApi._fnDraw(oSettings);
};
})(window.jQuery);
So you can use it from other places as:
var oTable = $('.dataTable').dataTable();
oTable.fnStandingRedraw();
To call fnStandingRedraw() from tapestry:
jsSupport.addScript("$(%s).dataTable().fnStandingRedraw()", "#myTable");
or you can add tapestry initializer and call it:
Tapestry.Initializer.standingRedraw = function(spec) {
$(spec.tableId).dataTable().fnStandingRedraw();
};
in java:
jsSupport.addInitializerCall("standingRedraw",
new JSONObject("tableId", "#myTable"));
But this will work for you only if your DataTable is client-side(pagination, sorting, filtering) and you do ajax request to delete row and delete this row from DataTable manually from js.
You have no oSettings object.
You can call it from the dataTable object (which has oSettings):
var dt = $("#table").dataTable();
dt.fnStandingRedraw();

Using jQuery to pull text from a specific <td>

I'm running an AJAX query on an external page, and am attempting to only return the data from the County . My current script is pulling the text from all of the table cells, but I cannot for the life of me get it to simply pull the county name.
The current script that is being run:
$( ".zipCode" ).each(function( intIndex ){
var zipCodeID = $(this).attr('id');
console.log('http://www.uscounties.org/cffiles_web/counties/zip_res.cfm?zip='+zipCodeID);
$.ajax({
url: 'http://www.uscounties.org/cffiles_web/counties/zip_res.cfm?zip='+zipCodeID,
type: 'GET',
success: function(res) {
var headline = $(res.responseText).find("p").text();
console.log(headline);
$('#'+zipCodeID).empty();
$('#'+zipCodeID).append(headline);
}
});
});
An example of the page that is being queried:
http://www.uscounties.org/cffiles_web/counties/zip_res.cfm?zip=56159
This should work for all entered ZIPS. The page layout is the same, I just can't get the function to return only the county. Any help or advice would be awesome. Thanks!
With the complete lack of ids and classes on that page, you don't really have much to go on. If you have access to the source of that page, stick an id or class on the cell and make your life so much easier. If not, you'll have to use what you know about the structure of the pages to find the county. Something like this will work specifically on that one page you linked to. If other pages have slight variations this will fail:
var headline = $(res.responseText).find("table > tr:eq(2) > td:eq(3)").text();
This assumes that there is only ever one table on the page and that the county is always in the 3rd cell of the 2nd row.
You're basically screen scraping. I somehow think you'll have issues with this due to cross domain and other things, but that is ancillary to the question.
You need to walk through the resultant page. Assuming there is only ever one page on the screen, it'll look like something this:
var retVal = [];
// Basically, for each row in the table...
$('tr').each(function(){
var pTR = $(this);
// Skip the header row.
if (pTR.find('th').length == 0)
{
// This is the array of TDs in the given row.
var pCells = $('td', pTR);
retVal.push({state:$(pCells[0]).text(), place:$(pCells[1]).text(), county:$(pCells[2]).text()});
}
});
// retVal now contains an array of objects, including county.
if (retVal.length > 0)
{
alert(retVal[0].county);
}
else
{
alert('Cannot parse output page');
}
The parsing code is written to be extensible, hence you get back all of the data. With postal codes, although you will likely only ever get back one county, you'll definitely get back more places. Also note... not every zip code has a county attached for a variety of reasons, but you should get back an empty string in that case.

Categories

Resources