jQuery DataTables filter plugin and QUnit

Here’s something about how to create a filter plugin for the jquery datatables plugin and then test it using qunit.

So recently, we had a need for responsive tables. Datatables seemed to be a good choice, since its got good documentation, its actively developed and has a load of plugins and extensions. It’s also easy to build your own extensions so that’s what I did. Here’s a zip of the complete code if you want to take a look

The problem

We needed to be able to add an arbitrary number of filters that would cumulatively filter the table. The filters would need to do different things. Some would simply toggle a switch on and off, others would add the selected filter to a filter list, some would allow selection/deselection of a range of filters.

So, I decided to create the filter extension that would be activated by data attributes on certain elements.

Solution overview

filters1

The basic solution is as follows: – The table that needs to have the enhanced filters has a data-attribute called ‘data-filter-enhanced.’ – Any filters need to sit inside a container with a data attribute of ‘data-filter-column’ which specifies which column the data will be filtered on. – Items inside that container need to have a data-attribute of ‘data-filter-value’ with a value corresponding to a potential value in the table.

  <div data-filter-column="state">
    <ul>
    <li><a href="#" data-filter-value="filter state 1">filter state 1 with some other markup</a></li>
    <li><a href="#" data-filter-value="filter state 2">filter state 2</a></li>
    </ul>
  </div>
  <table id="table1" data-filter-enhanced>
    <thead>
      <tr>
        <th>state</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>filter state 1</td>
      </tr>
      <tr>
        <td>filter state 2</td>
      </tr>
    </tbody>
  </table>

There are a load of extra bits that I’ll discuss later on but for now, that’s the basic idea.

Javascript

I’ve been trying to improve my javascript-fu recently and had been reading about various patterns, one of which is the revealing prototype pattern, a variation on the revealing module pattern. Instead of revealing an object of functions inside a wrapper object, the revealing prototype reveals an object of functions on the prototype of an object.

Initialising the filter

First, the filter initialisation needs to hook into the datatable init event. This is the code…

$(document).on('init.dt', function (e, settings, json) {
  if (e.namespace !== 'dt' || e.target.attributes['data-filter-enhanced'] === undefined) {
    return;
  }
  oe.enhancedFilter = new EnhancedFilter(e.target);
  oe.enhancedFilter.init();

});

So, it checks whether the event is actually the correct datatables init event and whether the target table has the data attribute ‘data-filter-enhanced.’ If these are true, the EnhancedFilter object is initialised. It’s actually created as an object of a single global ‘oe,’ which is our general namespace. The init event looks like this…

var EnhancedFilter = function (target) {
  this.table = target;
  this.activeFiltersCount = 0;
  this.activeFilters = {};
};

This bit takes a target table and initialises it with some instance variables. These are used to track which filters have been applied.

The prototype

Most of the functionality lives inside the prototype of the ‘EnhancedFilter’ object. The prototype defines a load of variables and functions and then exposes a single object at the end with functions that are public…

return {
  init: init,
  filterData: filterData
};

So, there’s a lot of functionality in the prototype but the only externally available functions are ‘init’ and ‘filterData.’ However, all the other internal functions are on a prototype level, so they are shared across all instances, not recreated for every instance.

Init function

This is basically a loop that goes through every element marked with a ‘data-filter-value’ or ‘data-filter-modifier’ attribute and carries out some processing.

filterElements = $('[data-filter-modifier],[data-filter-value]', '[data-filter-column]')
  .on('click', addRemoveFilterElement)
  .each(function () {
    var e = $(this),
      parent = e.parents('[data-filter-column]').first(),
      attr = parent.attr('data-filter-column'),
      val = e.attr('data-filter-value'),
      column = obj.tableApi.column(attr + ':name'),
      itemCount;
    if (obj.activeFilters[attr] === undefined) {
      obj.activeFilters[attr] = [];
    } 
    //if the filter has a value attribute, add the number of items
    if (val !== undefined && parent.attr('data-filter-no-count') === undefined) {
      itemCount = $.grep(column.data(), function (e) {
        var tempVal = val.toLowerCase().split(',');
        return tempVal.indexOf(e.toLowerCase()) > -1;
      }).length || 0;
      //add the count to each column filter
      $(countSpan.replace(/\%c/, itemCount)).appendTo(e);
    }   
    if (e.hasClass('is-active')) {
      addRemoveFilterElement.call(e);
    }
});

This does the following – adds an event listener for each filter item – adds an empty object into the activeFilters object for each filter that is found – calculates how many items exist for a given filter item.

On this last point, it later became a requirement to be able to filter multiple items using a single filter. For example, a filter list item could look like this…

<li><a href="#" data-filter-value="filter state 2, filter state 2b">filter state 2 and 2b</a></li>

By clicking this filter item, the table would be filtered by any row that contains a value of ‘filter state 2’ or ‘filter state 2b.’ The grep function in the code above splits the item value and compares each item against values that exist in the column. For this to work, each column must be given a name that corresponds to the data-filter-column attribute on the parent item. This is done like so…

$table.DataTable({
  columns: [
    { name: 'state'},
    { name: 'category'},
    { name: 'heading 3'}
  ]
});

This bit of code initialises the datatable with column headings that can be easily referenced in code.

After the check has been made, a span is added to each filter item showing the count. Finally, if any filter item is marked with a class of ‘is-active’ it is immediately selected. This means that a default filter can be added.

Adding and removing filters

So, when a filter item is clicked, processing needs to occur to select or deselect the filter, and then the datatable needs to be re-drawn. Initially this started out as a single function but quickly became un-manageable since different filters behaved differently. I therefore adopted the strategy pattern for handling how each filter would behave. Here’s a little code snippet to explain…

var processStrategy = function (name, target, filter) {
  if (strategies[name] !== undefined) {
    strategies[name].process(target, filter);
  }
};
if (parent.attr('data-filter-type') !== undefined) {
  processStrategy(parent.attr('data-filter-type'), target, filter);
}   
processStrategy('default', target, filter);

This bit of code allows different strategies to be managed more easily since they are encapsulated into a single object – ‘strategies.’ First, a check is made to see if the particular strategy exists. The strategy is defined as a data attribute on the filter item container. e.g.

<ul class="add-button-filters" data-filter-column="category" data-filter-type="addButton">

If the strategy does exist, its called. After that, the default filtering strategy is called and the table re-drawn. This initiates the actual filtering process.

Default filtering action

The default action is to toggle the ‘is-active’ state on the filter and either add or remove the selected filter values.

var filterValue = target.attr('data-filter-value');
if (filterValue === undefined) {
  return;
}    
//if any of the filter values are already in the filter array, remove them, else add them
filterValue = filterValue.toLowerCase().split(',');
if (getFilteredElements(filter, filterValue).length > 0) {
  addToFilterArray(target, filter, filterValue, false);
} else {
  addToFilterArray(target, filter, filterValue, true);
}    
  • The filter value is obtained from the clicked element
  • If there is no filter value, do nothing
  • Split out the values using ‘,’
  • The ‘getFilteredElements’ function is a grep using the selected filter compared against the values from the clicked element. The filter variable is the current list of filters corresponding to a table column. So if the table had a column ‘states’ the filter variable might look like this:

    [‘state 1’, ‘state 2’]

By calling ‘indexOf’ on that array using the selected value, -1 is returned if no value is found. This can then be used to either add or remove the element. So, say someone clicked on the ‘state 1’ filter button. ['state 1', 'state 2'].indexOf('state 1') would return 0 meaning that the value had been found. So, the operation in this case is to remove that filter (since it already exists). If the value wasn’t found, it would be added (make sense? good).

If any of the values were found, a call to ‘addToFilterArray’ is made…

addToFilterArray(target, filter, filterValue, true);

Where ‘target’ is the element clicked, ‘filter’ is the array of values that corresponds to the column being filtered, ‘filterValue’ is the list of filter values belonging to the selected element, and ‘true’ is whether to add or remove the elements. The function looks like this…

if (add !== true) {
  target.removeClass('is-active');
  $.each(filterValue, function (i, el) {
    filter.splice(filter.indexOf(el), 1);
  });
  obj.activeFiltersCount -= 1;
} else {
  target.addClass('is-active');
  $.each(filterValue, function (i, el) {
    filter.push(el);
  });
  obj.activeFiltersCount += 1;
}

If the items should be removed… – the ‘is-active’ is removed from the selected element – each filter value is removed from the filter list

If the items should be added, the opposite happens 🙂

The actual filter function

When the table is re-drawn, this is what happens…

filterData = function (columns, data) {
/*
check filters array, get the table heading check states against the column number of the current row if matches, return true, else return false     */
var selectedColumn,
  colIndex,
  filterRow = false;    
//if there are no active filters, return everything
if (obj.activeFiltersCount === 0) {
  return true;
}    
$.each(obj.activeFilters, function (k, v) {
  //if there are no filter values in this filter, continue
  if (v.length === 0) {
    return true;
  }    
  selectedColumn = obj.tableApi.column(k + ':name');
  if (selectedColumn.length !== 1) {
    return false;
  }
  colIndex = selectedColumn.index();        
  if (this.indexOf(data[colIndex].toLowerCase()) !== -1) {
    filterRow = true;
  } else {
    //if any row doesn't match the filter, return false and break out of loop
    filterRow = false;
    return false;
  }
});    
return filterRow;    

The code is commented but in essence, its looping through each row of the table, for each active filter, its finding the appropriate column, checking the value of the cell against any active filters and if a match is found it returns true, which means show the row. This implementation shows all rows of a table if there are no active filters. It also performs an ‘or’ rather than an ‘and’ on the active filters, meaning that if the cell value matches ‘any’ active filter it should be shown.

QUnit

To document and test the component, I decided to dig into QUnit, the testing framework developed by JQuery. After a little set up, its actually pretty intuitive to use.

Setup

  • QUnit is obviously needed. I grabbed it using node package manager.
  • The fixture file is needed, where the relevant files can be loaded and sample data can be added. The fixture file contains a div with an id of ‘qunit-fixture.’ Inside here, any html is re-instantiated for each test.
  • The test file is needed, that contains setup and teardown code, along with the actual tests.

The procedure

I’m not going to repeat what it says on the QUnit site since its very well documented. Basically, the code is organised into modules and tests. Wherever you specify a new module, every test below will be in that module until another module is defined. e.g.

QUnit.module('Filters basic selection', {
  beforeEach: beforeEach(),
  afterEach: afterEach()
});

This sets up a module and defines some common set up that needs to happen before and after each test. By abstracting them into separate functions, these don’t need to be repeated for every module unless things change. So, this is my common beforeeach and aftereach function…

beforeEach = function() {
  return function() {
    oe = {};
    $fixture = $('#qunit-fixture');
    $table = $('#table1', $fixture);
    $table.DataTable({
    columns: [
      { name: 'state'},
      { name: 'category'},
      { name: 'Header 3'},
      { name: 'Header 4'},
      { name: 'Header 5'}
    ]
    });
  };
},
afterEach = function() {
  return function() {
    $table.DataTable().destroy();
    oe = {};
  };
};

Here’s a sample test…

QUnit.test('Removing all filters shows the entire table', function(assert) {
  $('[data-filter-value^="filter state 1"]').click();
  assert.notStrictEqual($('tbody tr', $table).length, 8, 'table is filtered');
  $('[data-filter-value^="filter state 1"]').click();
  assert.strictEqual($('tbody tr', $table).length, 8, 'table shows all rows when all filters de-activated');
});

You can use jquery selectors and events to interact with the fixture and use them as well to test assertions. So in this case, a specific filter link is clicked once, clicked again and then a test is made on the table length.

Doing this is also a good way to document the code. On a number of occasions I also found it very useful when re-factoring code became necessary, since I could immediately see what had broken.

Conclusion

This exercise was about using the revealing prototype pattern, the strategy pattern and QUnit to develop, test and document a new filter componenent that could easily plug in to the datatables jquery plugin. There’s quite a lot that the module does that I’ve not talked about, such as different strategies for handing filters, modifiers that allow selection of a group of filters, or selection/deselection of all filters, but feel free to have a look at the code.

Leave a Reply