Alexander Farber Alexander Farber - 2 months ago 18
CSS Question

DataTables hidden row details example - the table header is misplaced (test case attached)

I'm trying to create a table where more details can be seen when the plus-image is clicked - similar to the DataTables hidden row details example

Unfortunately there is a warning being printed as JavaScript alert and also the table header is misplaced - as if there would be too many or not enough table cells in it:

enter image description here

I have prepared a simple test case, which will work instantly, when you save it to a file and open it in a browser:

<!DOCTYPE HTML>
<html>
<head>
<link type="text/css" rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/redmond/jquery-ui.css">
<link type="text/css" rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables_themeroller.css">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.min.js"></script>
<script type="text/javascript">

var data = [
{"Total":17,"A":0,"B":0,"Details":{"BSN":"1147387861","ProjectName":"R127","StationName":"D"},"C":0,"D":17,"Test":"GSM_1900_GMSK_TXPOWER_HP_H","Measurement":"MEASUREMENT"},
{"Total":8,"A":0,"B":0,"Details":{"BSN":"1147387861","ProjectName":"R127","StationName":"D"},"C":0,"D":8,"Test":"TX_PWR_64_54","Measurement":"POWER"}
];

$(function() {

function fnFormatDetails(oTable, nTr) {
var aData = oTable.fnGetData(nTr);
var sOut = '<table bgcolor="yellow" cellpadding="8" border="0" style="padding-left:50px;">';
sOut += '<tr><td>BSN:</td><td>' + aData['Details']['BSN'] + '</td></tr>';
sOut += '<tr><td>Station:</td><td>' + aData['Details']['StationName'] + '</td></tr>';
sOut += '<tr><td>Project:</td><td>' + aData['Details']['ProjectName'] + '</td></tr>';
sOut += '</table>';

return sOut;
}

var fails = $('#fails').dataTable({
bJQueryUI: true,
sPaginationType: 'full_numbers',
aaData: data,
aaSorting: [[2, 'desc']],
aoColumns: [
{ mDataProp: 'Test', bSearchable: true, bSortable: true },
{ mDataProp: 'Measurement', bSearchable: true, bSortable: true },
{ mDataProp: 'Total', bSearchable: false, bSortable: true },
{ mDataProp: 'A', bSearchable: false, bSortable: true },
{ mDataProp: 'B', bSearchable: false, bSortable: true },
{ mDataProp: 'C', bSearchable: false, bSortable: true },
{ mDataProp: 'D', bSearchable: false, bSortable: true },
]
});

var th = document.createElement('th');
var td = document.createElement('td');
td.innerHTML = '<img src="http://www.datatables.net/release-datatables/examples/examples_support/details_open.png" class="details">';

$('#fails tbody th').each(function() {
this.insertBefore(th, this.childNodes[0]);
});

$('#fails tbody tr').each(function() {
this.insertBefore(td.cloneNode(true), this.childNodes[0]);
});

$('#fails tbody').on('click', 'td img.details', function() {
var nTr = $(this).parents('tr')[0];
if (fails.fnIsOpen(nTr)) {
this.src = 'http://www.datatables.net/release-datatables/examples/examples_support/details_open.png';
fails.fnClose(nTr);
} else {
this.src = 'http://www.datatables.net/release-datatables/examples/examples_support/details_close.png';
fails.fnOpen(nTr, fnFormatDetails(fails, nTr), 'details');
}
});
});

</script>
</head>
<body>

<table id="fails" cellspacing="0" cellpadding="4" width="100%">
<thead>
<tr>
<th>Test</th>
<th>Measurement</th>
<th>Total</th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
</tr>
</thead>
<tbody>
</tbody>
</table>

</body>
</html>


Does anybody please have an idea, how to fix this?

I've tried adding/removing
<th>Details</th>
in the HTML body, but it didn't help.

I've also asked this question at the DataTables forum.

UPDATE:

I've received helpful comments by DataTables author and have decided to just prepend the plus-image to the contents of the first cell in each row - instead of adding a new cell to each row.

Unfortunately I have a new problem: the plus-image is displayed, but the orinigal text (the Test name) is gone:

enter image description here

Here is my new code (the plus-image is prepended by
propTest
):

<!DOCTYPE HTML>
<html>
<head>
<link type="text/css" rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1/themes/redmond/jquery-ui.css">
<link type="text/css" rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/css/jquery.dataTables_themeroller.css">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script type="text/javascript" src="https://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.4/jquery.dataTables.min.js"></script>
<script type="text/javascript">

var data = [
{"Total":17,"A":0,"B":0,"Details":{"BSN":"1147387861","ProjectName":"R127","StationName":"D"},"C":0,"D":17,"Test":"GSM_1900_GMSK_TXPOWER_HP_H","Measurement":"MEASUREMENT"},
{"Total":8,"A":0,"B":0,"Details":{"BSN":"1147387861","ProjectName":"R127","StationName":"D"},"C":0,"D":8,"Test":"TX_PWR_64_54","Measurement":"POWER"}
];

function propTest(data, type, val) {
if (type === 'set') {
console.log(val); // for some reason prints "null"
data.name = val;
data.display = '<img src="http://www.datatables.net/release-datatables/examples/examples_support/details_open.png" width="20" height="20" class="details"> ' + val;
return;
}

if (type === 'display') {
return data.display;
}

// 'sort', 'type', 'filter' and undefined
return data.name;
}

$(function() {

function fnFormatDetails(oTable, nTr) {
var aData = oTable.fnGetData(nTr);
var sOut = '<table bgcolor="yellow" cellpadding="8" border="0" style="padding-left:50px;">';
sOut += '<tr><td>BSN:</td><td>' + aData['Details']['BSN'] + '</td></tr>';
sOut += '<tr><td>Station:</td><td>' + aData['Details']['StationName'] + '</td></tr>';
sOut += '<tr><td>Project:</td><td>' + aData['Details']['ProjectName'] + '</td></tr>';
sOut += '</table>';

return sOut;
}

var fails = $('#fails').dataTable({
bJQueryUI: true,
sPaginationType: 'full_numbers',
aaData: data,
aaSorting: [[2, 'desc']],
aoColumns: [
{ mData: propTest, bSearchable: true, bSortable: true },
{ mData: 'Measurement', bSearchable: true, bSortable: true },
{ mData: 'Total', bSearchable: false, bSortable: true },
{ mData: 'A', bSearchable: false, bSortable: true },
{ mData: 'B', bSearchable: false, bSortable: true },
{ mData: 'C', bSearchable: false, bSortable: true },
{ mData: 'D', bSearchable: false, bSortable: true }
]
});

$('#fails tbody').on('click', 'td img.details', function() {
var nTr = $(this).parents('tr')[0];
if (fails.fnIsOpen(nTr)) {
this.src = 'http://www.datatables.net/release-datatables/examples/examples_support/details_open.png';
fails.fnClose(nTr);
} else {
this.src = 'http://www.datatables.net/release-datatables/examples/examples_support/details_close.png';
fails.fnOpen(nTr, fnFormatDetails(fails, nTr), 'details');
}
});
});

</script>
</head>
<body>

<table id="fails" cellspacing="0" cellpadding="4" width="100%">
<thead>
<tr>
<th>Test</th>
<th>Measurement</th>
<th>Total</th>
<th>A</th>
<th>B</th>
<th>C</th>
<th>D</th>
</tr>
</thead>
<tbody>
</tbody>
</table>

</body>
</html>

Answer

There are several ways to achieve this type of behavior with DataTables (DT). You can achieve your original goal or use the same column for the expander and the Test data.

I am taking the time to show several options, as I am hoping that this could serve as reference to others asking to do something similar.

For all of the following, your table should contain all of the desired columns prior to the DT's creation (either statically or dynamically).


Expander in a separate column

Using a default content property:

This is the option that requires the least amount of modification to your existing example and is probably the easiest. Just add a sDefaultContent option for your extra column:

{
    mData: null,
    bSearchable: false,
    bSortable: false,
    sDefaultContent: '<div class="expand /">',
    sWidth: "30px"
},

The mData property is set to null, as it its content has nothing to do with the row's data array. This makes DT to use the value in sDefaultContent.

I took the liberty of using CSS for the expander button, as this makes the example simpler and also decouples behavior and presentation.

The CSS I used:

tr div.expand {
  width: 20px;
  height: 20px;
  background-image: url('http://www.datatables.net/release-datatables/examples/examples_support/details_open.png');
}

tr div.open {
  background-image: url('http://www.datatables.net/release-datatables/examples/examples_support/details_close.png');  
}

and the expander function:

$('#fails tbody').on('click', 'td div.expand', function () {
    var nTr = $(this).parents('tr')[0];
    if (fails.fnIsOpen(nTr)) {
        $(this).removeClass('open');
        fails.fnClose(nTr);
    } else {$.fn.dataTableExt.sErrMode = 'throw' ;
        $(this).addClass('open');
        fails.fnOpen(nTr, fnFormatDetails(fails, nTr), 'details');
    }
});

We target a div with an expand class, and toggle its open class as needed.

Separate expand column

JSBin example.


Expander contained in the first column

Using a Formatter:

You can specify an aoColumnDefs config array, which is more powerful and flexible than the aoColumns config.

Specifying a renderer function for that column is done via themRender config property.

The relevant column's settings:

aoColumnDefs: [{
    aTargets: [0],
    mData: 'Test',
    bSearchable: true,
    bSortable: true,
    mRender: expandRenderer
},
 ...
]

That function is called multiple times in different contexts and should tell the DT how to display the data in the table itself, as well as help determine the type of data for sorting and filtering purposes.

Renderer:

function expandRenderer(data, type, full) {
    switch (type) {
        case 'display':
          return '<div class="expand-wrapper">'+
            '<span class="expand"></span>'+
            '<span class="data">'+
              data+
            '</span></div>';
        case 'type':
        case 'filter':
        case 'sort':
            return data;
    }
}

It takes 3 arguments:

  • data: The data from the row's array, in this case, the string stored in Test.
  • type: The type of request. It denotes what the DT expects to get back:
    • 'display': what will be displayed in the cell itself.
    • 'type': what returned from the function will be used to determine the row's data type.
    • 'filter', 'sort': The data to be used in filration (search) and sorting of the table.
  • full: The complete data for this row.

This time, I decided to use spans wrapped by a div. In order to center the icon vertically and have the table keep everything in one line, I used the following CSS:

tr span.expand {
    width: 20px;
    height: 20px;
    background-image: url('http://.../details_open.png');
    display:inline-block;
    vertical-align: middle;
    margin-right: 5px;
}

div.expand-wrapper {
    white-space:nowrap;
}

vertical-align: middle here works as intended, since this is an inline display (inline-block, specifically).

Without this CSS, DT wouldn't handle the column widths as gracefully, as the first column is not really aware of the extra space taken by the expander.

Without this CSS: Expander in same column

With this CSS:

Expander in same column and same line

And the corresponding JSBin example.

Tip:

I dislike the use of alert() to display errors. For DT, you can change this behavior by changing its error mode setting:

$.fn.dataTableExt.sErrMode = 'throw' ;

This will throw an exception, so you can catch it JIT using your console, or have it logged if uncaught.

Resources:

DataTable column options.

DataTable options quick reference.


PS,

Further customization can be done by passing a function as the mData configuration parameter, but this is beyond the scope of this answer.

Comments