I’m currently working on a piece of software where one view on the data is a list view, basically an HTML table. I’m displaying one row per item and one attribute of this item per column. Additionally, the header had two lines: the actual column header and a filter field (where you can type in a string which will filter the rows displayed). So it looks like this (with of course more rows):
<table id="list-view-table"> <thead> <tr> <th>Actions</th> <th>Type</th> <th>Number</th> <th>Task</th> <th>Owner</th> <th>Due Date</th> <th>Creation Date</th> <th>Priority</th> </tr> <tr> <td>...</td> <td>...</td> <td>...</td> <td>...</td> <td>...</td> <td>...</td> <td>...</td> <td>...</td> </tr> </thead> <tbody> <tr> <td> <a onclick="deleteItem(113528234059682)" href="#" title="Delete"><img src="delete.png"></a> <a onclick="cloneItem(113528234059682)" href="#" title="Clone"><img src="clone.png"></a> </td> <td> <select> <option value="type1" selected="selected">Type 1</option> <option value="type2">Type 2</option> </select> </td> <td>3</td> <td></td> <td>Henri</td> <td></td> <td>2013-06-28</td> <td>1</td> </tr> </tbody> </table>
Of course I’m kind of oversimplifying. The rows and cells would have different classes and IDs, but those are not relevant here.
Now what happens is that multiple columns are either empty because the attribute is not set for any item or all contain the same value. In the example above I only have 8 columns and you could end up having 20 columns. Many of them containing the same value in all cells. It makes things not very readable and these columns actually do not really help much, since the user actually knows that all items have the same value for an attribute.
So I started implemented a piece of PHP code to only add columns to the table which do not have the same value. Sounded like a great idea. The problem is that on the page, you can also edit an item and thus change the values. So a column might need to be added because after the update of an item, not all cells in the column have the same value. So I built in a reload of the page whenever an item was updated. But it caused the filtering and sorting to be lost. So I needed a JavaScript solution for this.
First we need a function which can be called once the page is loaded and every time an item is updated:
function hideOneValueColumns(table_id) { }
I provide the ID of the table as parameter. There are a few different things I additional need:
- First I do not want to consider the rows with headers or filters when evaluating whether all cells in the column have the same value. For the column header, it’s easy because it’s a different element type (<th/> instead of </td>). For the filters, I’ll just assign the row a specific class and add a parameter to the function to ignore this row.
- Second I have columns with actions which should always be displayed and a some columns I want to always be displayed (like the type which is a select box and needs to be there for the user to be able to change it). The solution is the same: Just add a new parameter with a class to ignore.
Here’s the current signature of the function:
function hideOneValueColumns(table_id, ignore_row_class, ignore_column_class) { }
Almost done, only need some logic in the function ! 😉
First we’ll go through the column. One of the issues we have here is that columns are not directly modeled in the table. Instead a table contains multiple rows which contain cells. These cells are vertically align. So they display as columns but you cannot just select a given column directly. Moreover cells can span multiple columns and/or rows.
Disclaimer 1: I assume the table does not contain any cell spanning multiple rows or columns. It is the case in my example and it makes everything easier.
So to go through the columns means going through the cells of a given row and then considering the cells below them in the other rows. So let’s first start with the column header as it is anyway using a different HTML element:
$('#'+table_id+' th').each(function(i) { }
In this function, we get a column index (or rather a cell index in this row). We can then use it to find the cells of the same columns in the rows of the table.
But first we need to check whether this column is one of the columns we wanted to ignore. One way to do it would be to extend the selector to not select these columns e.g.:
$('#'+table_id+' th:not(.'+ignore_column_class+')')
The problem is that the index you then get is relative to the selector you use. If you exclude cells in the header using a selector, you’ll need to fo the same afterwards for the other rows. So the cells in the other rows need to have the same class. I didn’t want that. So I implemented it this way:
function hideOneValueColumns(table_id, ignore_row_class, ignore_column_class) { //first go through the column headers $('#'+table_id+' th').each(function(i) { //only process the column if it isn't one of the column we should ignore if (!$(this).hasClass(ignore_column_class)) { } }); }
Now we need to go through the cells of the columns and hide them or show them depending on the values of all cells in the column. But we need to consider the rows we are to ignore. On the other hand, we need to hide and show all cells in the columns no matter whether they are to be evaluated or not. So I’m just getting them with two similar selectors (you could of course get the first one and create the second list by filtering it but I do not think it’d be much faster or less resource intensive):
//get all cells of the columns (in order to show or hide them) var all_cells = $(this).parents('table').find('tr td:nth-child(' + (i + 1) + ')'); //get the cells we'll use to decide whether we want to filter the column or not var filtered_cells = $(this).parents('table').find('tr:not(.'+ignore_row_class+') td:nth-child(' + (i + 1) + ')');
In order to find out whether all cells have the same value, we’ll just go through the cells (ignoring the rows as specified) and gather the different cell values:
//array containing the list of different values in a column var values = new Array(); filtered_cells.each(function() { var value = this.innerHTML; if (values.indexOf(value) == -1) { values.push(value); } });
If the value is not already in the array, just add it. Then we only have to show all cells in the column (including column headers) if there are at least 2 different values:
if (values.length < 2) { $(this).hide(); all_cells.hide(); } else { $(this).show(); all_cells.show(); }
So we basically hide or show the column header (the <th> tag selected above) and all cells of the column.
That’s it. The complete code looks like this:
function hideOneValueColumns(table_id, ignore_row_class, ignore_column_class) { //first go through the column headers $('#'+table_id+' th').each(function(i) { //only process the column if it isn't one of the column we should ignore if (!$(this).hasClass(ignore_column_class)) { //get all cells of the columns (in order to show or hide them) var all_cells = $(this).parents('table').find('tr td:nth-child(' + (i + 1) + ')'); //get the cells we'll use to decide whether we want to filter the column or not var filtered_cells = $(this).parents('table').find('tr:not(.'+ignore_row_class+') td:nth-child(' + (i + 1) + ')'); //array containing the list of different values in a column var values = new Array(); //gather the different cell values filtered_cells.each(function() { var value = this.innerHTML; if (values.indexOf(value) == -1) { values.push(value); } }); //hide if less than 2 different values and show if at least 2 different values if (values.length < 2) { $(this).hide(); all_cells.hide(); } else { $(this).show(); all_cells.show(); } } }); }
You can call this function like this:
hideOneValueColumns('list-view-table', 'filters', 'no-hide');
Where “filters” is the class of the rows you want to ignore and “no-hide” the class applied to all columns which should not be affected by this function.
Another thing you could had: check whether the number of rows is more than 1 (in this case this whole thing makes no sense). In order to do it, you need to first find the number of rows (other than the one which should be ignored). Since we assume there is a column header row, we want to check whether we have more than 2 rows. Here’s how to compute the number of rows:
var row_count = $('#'+table_id+' tr:not(.'+ignore_row_class+')').length;
If it is greater than 2, we do the same as above. Otherwise, we will just show all cells (since they might have been hidden previously):
$('#'+table_id+' th').show(); $('#'+table_id+' tr td').show();
Now the whole code:
function hideOneValueColumns(table_id, ignore_row_class, ignore_column_class) { var row_count = $('#'+table_id+' tr:not(.'+ignore_row_class+')').length; if (row_count > 2) { //first go through the column headers $('#'+table_id+' th').each(function(i) { //only process the column if it isn't one of the column we should ignore if (!$(this).hasClass(ignore_column_class)) { //get all cells of the columns (in order to show or hide them) var all_cells = $(this).parents('table').find('tr td:nth-child(' + (i + 1) + ')'); //get the cells we'll use to decide whether we want to filter the column or not var filtered_cells = $(this).parents('table').find('tr:not(.'+ignore_row_class+') td:nth-child(' + (i + 1) + ')'); //array containing the list of different values in a column var values = new Array(); //gather the different cell values filtered_cells.each(function() { var value = this.innerHTML; if (values.indexOf(value) == -1) { values.push(value); } }); //hide if less than 2 different values and show if at least 2 different values if (values.length < 2) { $(this).hide(); all_cells.hide(); } else { $(this).show(); all_cells.show(); } } }); } else { $('#'+table_id+' th').show(); $('#'+table_id+' tr td').show(); } }