Using jQuery to Filter an HTML Table

I hate long lists – well – I don’t hate them, because they show me everything I need to see; what I hate is long lists that cannot be whittled down with a nice filter system. To remedy this, I created a Live Filter function in jQuery that enables filtering on a table. I chose a table because GridViews – which are a control within the .NET framework that render out as tables – are commonly used for outputting long lists of tabulated data.
This is all well and good I hear you say, but what the hell is a live filter? I suppose it’s essentially like a search facility except instead of running server-side and querying a database it queries the markup and runs on the client-side which makes it very very fast!

View a Demonstration

In order for this to work we need to think about a few things:

  • There will have to be a form field that will allow us to grab the user’s search string
  • We will need to extract data from the table to create our own datasource that we can search and query (we’ll use an array)
  • A way of matching the users search string to the content within the table

First thing’s first then. Let’s create the markup with the Search form, (in the interest of saving wear on your mouse wheels I have reduced the number of results in the table):

<div id="liveFilter">
	<div class="liveFilterContainer">
		<input type="text" class="liveFilterInput default" value="Live Filter" />
		<a href="#" class="clearField" title="Clear Filter">x</a>
	</div>
	<div class="noResults"><strong>Sorry.</strong> There is no match for your filter; please try again.</div>
	<table class="liveFilterList" border="0">
		<tbody>
			<tr>
				<th>Filename</th>
				<th>Type</th>
				<th>Date</th>
			</tr>
			<tr>
				<td>The Hitch Hiker's Guide to the Galaxy</td>
				<td>Book</td>
				<td>28/01/2011</td>
			</tr>
			<tr>
				<td>The Hitch Hiker's Guide to the Galaxy</td>
				<td>Film</td>
				<td>28/01/2011</td>
			</tr>
			<tr>
				<td>Dirk Gently's Holistic Detective Agency</td>
				<td>TV Series</td>
				<td>28/01/2011</td>
			</tr>
		</tbody>
	</table>
</div>

I’m not going to cover the styles used in the interest of saving time. The styles are available in the demonstration file linked above.
Last and most important of all is the jQuery goodness which will turn this lifeless table into a thing of unparalleled functionality.

jQuery(document).ready(function(e) {
	
	var columnsToIndex = [1,2]; // first column is 1
	
	initLiveFilter(columnsToIndex);
});

// Live Filter
function initLiveFilter(columnsToIndex) {

	var liveFilter = jQuery("#liveFilter");
	
	if (liveFilter.length > 0) {
		
		var	liveFilterField 	= liveFilter.find("input.liveFilterInput"),
			liveFilterGrid		= liveFilter.find("table.liveFilterList"),
			liveFilterGridRows 	= liveFilterGrid.find("tr:gt(0)"), // gt(0) prevents the first row (normally a TH) from being included.
			liveFilterClear 	= liveFilter.find(".clearField"),
			liveFilterNoResults = liveFilterGrid.prev(),
			liveFilterDataArray = new Array(),
		
	//Create an array and populate it with key codes that should not cause shake effects
	
			characterValidationArray = [8,45,46], //backspace, insert, delete
		
	//Create an array and populate it with key codes that don't trigger an action
		
			characterExclusionArray = [13,20,27,33,34,37,39,35,36,16,17,18,144,145]; // enter, caps, esc, page up, page down, left, right, home, end, shift, ctrl, alt, num lock, scroll lock
		
	// Create the datasources we'll use to index the content
	
		if (columnsToIndex.length > 1) {
			
			for (col=0; col<=columnsToIndex.length-1; col++)
			{
	
				liveFilterGridRows.children("td:nth-child(" + columnsToIndex[col] + ")").each(function(i){
				
					liveFilterDataArray[liveFilterDataArray.length++] = jQuery(this).text();
				
				});
			}
			
		} else {
			
			liveFilterGridRows.children("td:first-child").each(function(i){
				
				liveFilterDataArray[i] = jQuery(this).text();
				
			});
		}
	
	// When a key is pressed in the designated input field - do the following:
	
		liveFilterField.on("keyup",function(key){
			
		// Check the character that was pressed and ensure it doesn't exist in the exclusion array defined above
	
			if (jQuery.inArray(key.keyCode,characterExclusionArray) == -1) {
			
				var	liveFilterValue = liveFilterField.val();
	
				if (!liveFilterField.hasClass("default")){
				
					if (liveFilterValue != "") {
	
						liveFilterClear.fadeIn(300);
		
						rowsToShow = new Array();
						
						var currentRow = 0;
						
					// Check the value entered against a regular expression matched with the column data. If a match exists add the row to a new array
	
						for (var i=0; i < liveFilterDataArray.length; i+=1) {
		
							RE = eval("/" + liveFilterValue + "/i");
			
							if (liveFilterDataArray[i].match(RE)) {
								
								rowsToShow.push(currentRow);
								
							}
							
							if(currentRow < liveFilterGridRows.length - 1) {
		
								currentRow++;
								
							} else {
		
								currentRow = 0;
		
							}
						}
					
					// If there are matches, show the grid, hide all the rows and show the selected ones
	
						if (rowsToShow.length > 0) {
	
							liveFilterGrid.show();
							liveFilterGridRows.hide();
							if (liveFilterNoResults.is(":visible")) {
								liveFilterNoResults.slideUp(150);
							}
							
							for (var i=0; i < rowsToShow.length; i+=1) {
	
								jQuery(liveFilterGridRows.get(rowsToShow[i])).show();
	
							}
							
					// If there are NO matches we hide the grid and display an error panel. If an incorrect value is entered while the error panel is visible it shakes itself assuming if 										it doesn't match any of the excluded values defined in the characterValidationArray
							
						} else {
							
							liveFilterGrid.hide();
	
							// if the no results panel is shown and the effects queue length is 0 and there are no illegal character presses
													
							if (liveFilterNoResults.is(":visible") && liveFilterNoResults.queue().length == 0 && jQuery.inArray(key.keyCode,characterValidationArray) == -1) {
								
								liveFilterNoResults.effect('shake', {times:3, distance:3}, 100);
	
							} else {
								liveFilterNoResults.slideDown(150);	
							}
						}
						
					// If the value entered is blank, hide the clear field button, show the grid and all of its rows and hide the no results panel if it is visible
					
					} else {
	
						clearField();
					}
				}
			}
		});
		
		// When the designated input field is clicked do the following:
	
		liveFilterField.on("focus", function(){
	
			if (liveFilterField.hasClass("default")){
				liveFilterField.val("").removeClass("default");
			}
			
			return false;
	
		});
		
		// When the clear field link is clicked do the following:
		
		liveFilterClear.on("click",function(){
	
			clearField();
			
			return false;
	
		});
		
		// The clear field function which clears the search field under certain conditions
		
		function clearField() {
	
			liveFilterField.val('');
			liveFilterClear.fadeOut(300);
			liveFilterNoResults.slideUp(300);
			liveFilterGridRows.show();
			liveFilterGrid.show();
		}
	}
}

Okay, this is a long script, I’ll grant you that but don’t let it scare you it’s really very simple. Let’s break it down into manageable chunks:

jQuery(document).ready(function(e) {
	
	var columnsToIndex = [1,2]; // first column is 1
	
	initLiveFilter(columnsToIndex);
});

First up we’re going to set up a the ready event. This will trigger when all the DOM elements on the page have finished loading and will run the initLiveFilter function, but we’ll come back to that in a moment. We first set up a variable called columnsToIndex and define it as an array. This array will contain the columns you want to index in the table, our example has three columns, left to right these are numbered 1,2 and 3; one being the first column three being the last. We are going to index columns 1 and 2 (or Filename and Type if you are looking at the markup) so will set up a literal array as the columnsToIndex variable: [1,2] Next we called a function called initLiveFilter and pass the columnsToIndex variable to it so the function can get hold of those values.

function initLiveFilter(columnsToIndex) {

	var liveFilter = jQuery("#liveFilter");
	
	if (liveFilter.length > 0) {
		
		var	liveFilterField 	= liveFilter.find("input.liveFilterInput"),
			liveFilterGrid		= liveFilter.find("table.liveFilterList"),
			liveFilterGridRows 	= liveFilterGrid.find("tr:gt(0)"), // gt(0) prevents the first row (normally a TH) from being included.
			liveFilterClear 	= liveFilter.find(".clearField"),
			liveFilterNoResults = liveFilterGrid.prev(),
			liveFilterDataArray = new Array(),
		
	//Create an array and populate it with key codes that should not cause shake effects
	
			characterValidationArray = [8,45,46], //backspace, insert, delete
		
	//Create an array and populate it with key codes that don't trigger an action
		
			characterExclusionArray = [13,20,27,33,34,37,39,35,36,16,17,18,144,145]; // enter, caps, esc, page up, page down, left, right, home, end, shift, ctrl, alt, num lock, scroll lock

We start by defining the function initLiveFilter and give it a parameter of columnsToIndex. This will pass the columnsToIndex variable we defined in the block above to the function.

Next we set the liveFilter variable to a jQuery object and using an ID selector find the live filter in the mark up. Next, we use the liveFilter variable and check its length to see if it was found and if it was we further define a set of variables that we’ll use throughout the script.

  • liveFilterField: will be the input where the user can type there search string.
  • liveFilterGrid: is the table we will be searching.
  • liveFilterGridRows: are the rows that make up the table but by using (“tr:gt(0)”) as a selector we don’t select the first row which is usually a header.
  • liveFilterClear: is the button we will use to reset the search.
  • liveFilterNoResults: is a panel that we will display if the search string doesn’t match any values.
  • liveFilterDataArray: is our custom made data source that we shall use to store the content.

Next we’ll create another literal array called¬†characterValidationArray¬† which will contain a series of numbers that relate to key presses on a keyboard. When these keys are pressed it will not trigger the validation that we will apply to the input field later on.

After that we shall create another literal array called characterExclusionArray. This will contain key presses that won’t trigger the filter event. This means any key we press hear will perform its default action i.e. delete will delete tab will tab etc.

if (columnsToIndex.length > 1) {
	
	for (col=0; col<=columnsToIndex.length-1; col++)
	{

		liveFilterGridRows.children("td:nth-child(" + columnsToIndex[col] + ")").each(function(i){
		
			liveFilterDataArray[liveFilterDataArray.length++] = jQuery(this).text();
		
		});
	}
	
} else {
	
	liveFilterGridRows.children("td:first-child").each(function(i){
		
		liveFilterDataArray[i] = jQuery(this).text();
		
	});
}

Our next block sets up our datasources based on the columns we defined in the columnsToIndex variable. It first checked that the columnsToIndex variable is an array and has at least 1 value. If it is not defined then it will just set a datasource up using the first column of the table only.

Next, we use a for loop to iterate through each item in our columnsToIndex array, then then iterate over every row in the table and capture the columns text value and append it to the liveFilterDataArray.

With me so far?

liveFilterField.on("keyup",function(key){
	
// Check the character that was pressed and ensure it doesn't exist in the exclusion array defined above

	if (jQuery.inArray(key.keyCode,characterExclusionArray) == -1) {
	
		var	liveFilterValue = liveFilterField.val();

		if (!liveFilterField.hasClass("default")){
		
			if (liveFilterValue != "") {

				liveFilterClear.fadeIn(300);

				rowsToShow = new Array();
				
				var currentRow = 0;
				
			// Check the value entered against a regular expression matched with the column data. If a match exists add the row to a new array

				for (var i=0; i < liveFilterDataArray.length; i+=1) {

					RE = eval("/" + liveFilterValue + "/i");
	
					if (liveFilterDataArray[i].match(RE)) {
						
						rowsToShow.push(currentRow);
						
					}
					
					if(currentRow < liveFilterGridRows.length - 1) {

						currentRow++;
						
					} else {

						currentRow = 0;

					}
				}
			
			// If there are matches, show the grid, hide all the rows and show the selected ones

				if (rowsToShow.length > 0) {

					liveFilterGrid.show();
					liveFilterGridRows.hide();
					if (liveFilterNoResults.is(":visible")) {
						liveFilterNoResults.slideUp(150);
					}
					
					for (var i=0; i < rowsToShow.length; i+=1) {

						jQuery(liveFilterGridRows.get(rowsToShow[i])).show();

					}
					
			// If there are NO matches we hide the grid and display an error panel. If an incorrect value is entered while the error panel is visible it shakes itself assuming if 										it doesn't match any of the excluded values defined in the characterValidationArray
					
				} else {
					
					liveFilterGrid.hide();

					// if the no results panel is shown and the effects queue length is 0 and there are no illegal character presses
											
					if (liveFilterNoResults.is(":visible") && liveFilterNoResults.queue().length == 0 && jQuery.inArray(key.keyCode,characterValidationArray) == -1) {
						
						liveFilterNoResults.effect('shake', {times:3, distance:3}, 100);

					} else {
						liveFilterNoResults.slideDown(150);	
					}
				}
				
			// If the value entered is blank, hide the clear field button, show the grid and all of its rows and hide the no results panel if it is visible
			
			} else {

				clearField();
			}
		}
	}
});

This next section is BIG, but this is where all the magic happens! Using the keyup event and passing a key parameter into the keyup function we listen for when a key is released. We can then check to see if the key.code exisits in the exclusion array and if it doesn’t (it will return a value of -1 if it doesn’t find it) it will proceed to check if the input field has a value and then has a class of default. If the class of default exists, it means a search has not been carried out since the page last posted back.

We first fade in the live filter clear button over a 300ms duration.

We set up a new array called rowsToShow. This will contain all the rows in the table that match the current search string.

Then we set up a currentRow variable and set it’s initial value to 0.

The next thing we do is create another for loop, this will iterate over our datasource array (liveFilterDataArray) and will evaluate the liveFilterValue and using a regular expression will try and match it with the current value from the datasource. If there is a match, it takes the current value from the datasource and pushes it to the rowsToShow array we defined before the loop.

One the loop has finished, we check the rowsToShow array’s length and make sure it is greater than 0. If it is then we make sure the grid is visible, then we hide all the grid rows and slide up the no results panel. Then we show all the rows that matched the search string.

If there were no matches in the grid, we hide the grid, show the no results panel if its the first time no match was found and if the user continues to type we shake the no results box to draw their attention.

At the end of the block there is an else statement that runs another function called clearField. This basically means that if the input field is blank or has no value, then set everything back the way it was.

liveFilterField.on("focus", function(){

	if (liveFilterField.hasClass("default")){
		liveFilterField.val("").removeClass("default");
	}
	
	return false;

});

Next, we set up a few event listeners the first of which listens for focus on the input field. If a user focuses i.e. clicks in or tabs into the search field this event fires and triggers a function which checks if the input box has a class of default (which means its not been used yet) and removes the default class.

liveFilterClear.on("click",function(){

	clearField();
	
	return false;

});

Our last event: when a user clicks on the clear button, we call the clearField function which resets everything back to the way it was.

And last of all we have the clearField() function:

function clearField() {

	liveFilterField.val('');
	liveFilterClear.fadeOut(300);
	liveFilterNoResults.slideUp(300);
	liveFilterGridRows.show();
	liveFilterGrid.show();
}

Here we put everything back the way it was. We set the value of the liveFilterField to blank. We hide the liveFilterClear button. We hide the no results panel, show all the grid rows and the grid.

I know its a long script, but its not that complicated when we look at it in chunks. I hope you can make use of it.

6 thoughts on “Using jQuery to Filter an HTML Table

  1. Is it possible to have multiple search inputs. so as per the demo, in the first input box I’d search for books, in the second I would search for date showing all books for a specific date. then thirdly I can fiilter that list for the exact set of results? this would be awesome. Thanks,

Leave a Reply

Your email address will not be published. Required fields are marked *