8

How To Save Custom Filter State with JQuery Data Tables

Posted (Updated ) in Javascript, PHP

EDIT 2013-05-31: Updated for Data Tables 1.9.4 support

JQuery Data Tables is an incredibly handy tool that can make a developers life alot easier – notably by handling search, pagination, filtering and sorting for you. The default functionality is very good, however you’ll often need a bit of customization. This post will detail how to add custom filters and position them to nicely theme with your table. The filters’ state will also be saved so they’ll still be there if you reload the page.

Here’s the final product (Select a filter and reload the page to test state saving):

Flash of Unstyled Content

The default dataTables setup results in whats called a ‘flash of unstyled content’. This is where upon loading the page you’ll see the HTML table for a split second before it turns into the JQuery Data Table. This is very jarring and looks unprofessional. My quick solution to this problem is to simply hide the table until it’s time to be displayed using some simple CSS and the fnDrawCallback callback. Note that I’ve given my table a class of dtable.

CSS:

1
.dtable {display:none}

JS:

1
2
3
4
5
6
7
$(document).ready(function() {
	$('.dtable').dataTable({
		"fnDrawCallback":function(){
			$(this).show();
		}
	});
});

HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<table width='100%' class='dtable' cellspacing='0' cellpadding='0'>
<thead>
	<tr>
		<th>Name</th>
		<th>Gender</th>
		<th>Favourite Colour</th>
	</tr>
</thead>
<tbody>
	<tr>
		<td>Jeremy Smith</td>
		<td>Male</td>
		<td>Red</td>
	</tr>
	<tr>
		<td>Anita Hanson</td>
		<td>Female</td>
		<td>Orange</td>
	</tr>
	<tr>
		<td>Tim Burchall</td>
		<td>Male</td>
		<td>Orange</td>
	</tr>
	<tr>
		<td>Arnold Tamzarian</td>
		<td>Male</td>
		<td>Green</td>
	</tr>
	<tr>
		<td>Lisa Douglas</td>
		<td>Female</td>
		<td>Orange</td>
	</tr>
	<tr>
		<td>Mark Feeney</td>
		<td>Male</td>
		<td>Green</td>
	</tr>
	<tr>
		<td>Jessica Nicholson</td>
		<td>Female</td>
		<td>Red</td>
	</tr>
	<tr>
		<td>Jessica Williams</td>
		<td>Female</td>
		<td>Red</td>
	</tr>
	<tr>
		<td>Mia Huffson</td>
		<td>Female</td>
		<td>Green</td>
	</tr>
	<tr>
		<td>Jeremiah Evans</td>
		<td>Male</td>
		<td>Red</td>
	</tr>
</tbody>
</table>

Adding the Filters

I’ll be making 2 custom filters; one for gender and one for favourite colour which return any matching row in the table upon selection. You can make more complicated filters using the below code if you wish, this is only an example.

HTML (To be placed just before the <table> tag):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class='dtable_custom_controls'>
	<div class='dataTables_wrapper'>
		<div class='ui-toolbar'>
			<div class='dataTables_length'>
				<select name='cust_filter_gender' class='dtable_filter'>
					<option value=''>Gender</option>
					<option value='Male'>Male</option>
					<option value='Female'>Female</option>
				</select>
 
				<select name='cust_filter_colour' class='dtable_filter'>
					<option value=''>Colour</option>
					<option value='Green'>Green</option>
					<option value='Orange'>Orange</option>
					<option value='Red'>Red</option>
				</select>
			</div>
		</div>
	</div>
</div>

This snippet contains our two drop down lists encapsulated within the standard data tables’ header wrappers so that we get some nice theming/element sizing going on. It’ll currently be sitting above the table, but with a touch of CSS we can position it inside the header:

1
2
3
.dtable {display:none}
.dtable_custom_controls {display:none;position: absolute;z-index: 50;margin-left: 150px;margin-top:1px}.dtable_custom_controls .dataTables_length {width:auto;float:none}

Let’s also update our javascript to make this thing a bit prettier:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$(document).ready(function() {
	options = {
		"sDom"			: 'R<"H"lfr>t<"F"ip<',
		"bProcessing"		: true,
		"bJQueryUI"		: true,
		"bStateSave"		: true,
		"iDisplayLength" 	: 8,
		"aLengthMenu" 		: [[8, 25, 50, -1], [8, 25, 50, "All"]],
		"sPaginationType"	: "full_numbers",
		"aaSorting" 		: [[ 0, "asc" ]],
		"fnDrawCallback"	: function(){
			//Without the CSS call, the table occasionally appears a little too wide
			$(this).show().css('width', '100%');
			//Don't show the filters until the table is showing
			$(this).closest('.dataTables_wrapper').prevAll('.dtable_custom_controls').show();
		}
	};
	var oTable = $('.dtable').dataTable(options);
});

Now we need to actually make them do something. This is where fnStateSaveCallback and fnStateLoadCallback come in. The final script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
$(document).ready(function() {
	/*
	 * Find and assign actions to filters
	 */
	$.fn.dataTableExt.afnFiltering = new Array();
	var oControls = $('.dtable').prevAll('.dtable_custom_controls:first').find(':input[name]');
	oControls.each(function() {
		var oControl = $(this);
 
		//Add custom filters
		$.fn.dataTableExt.afnFiltering.push(function( oSettings, aData, iDataIndex ) {
			if ( !oControl.val() || !oControl.hasClass('dtable_filter') ) return true;
 
			for ( i=0; i<aData.length; i++ )
				if ( aData[i].indexOf(oControl.val()) != -1 )
					return true;
 
			return false;
		});
	});
 
	options = {
		"sDom"				: 'R<"H"lfr>t<"F"ip<',
		"bProcessing"		: true,
		"bJQueryUI"		: true,
		"bStateSave"		: true,
		"iDisplayLength"	: 8,
		"aLengthMenu"		: [[8, 25, 50, -1], [8, 25, 50, "All"]],
		"sPaginationType"	: "full_numbers",
		"aaSorting"			: [[ 0, "asc" ]],
		"fnDrawCallback"	: function(){
			//Without the CSS call, the table occasionally appears a little too wide
			$(this).show().css('width', '100%');
			//Don't show the filters until the table is showing
			$(this).closest('.dataTables_wrapper').prevAll('.dtable_custom_controls').show();
		},
		"fnStateSaveParams": 	function ( oSettings, sValue ) {			//Save custom filters			oControls.each(function() {				if ( $(this).attr('name') )					sValue[ $(this).attr('name') ] = $(this).val().replace('"', '"');			});			return sValue;		},		"fnStateLoadParams"	: function ( oSettings, oData ) {			//Load custom filters			oControls.each(function() {				var oControl = $(this); 				$.each(oData, function(index, value) {					if ( index == oControl.attr('name') )						oControl.val( value );				});			});			return true;		}	};
	var oTable = $('.dtable').dataTable(options);
 
	/*
	 * Trigger the filters when the user interacts with them
	 */
	oControls.each(function() {
		$(this).change(function() {
			//Redraw to apply filters
			oTable.fnDraw();
		});
	});
});

I won’t go over the above code as it should be pretty easy to figure out from the JS comments, however it’s worth mentioning that I’ve only set it up in such a way that you’ll be able to place 1 filtered table on each page. If you require more than one you’ll need to tweak the javascript a little.

The demo page for this post can be found here.