DataTables Helper Module Creation

I’ve been adventuring, not for the first time, into the world of Datatables. I’ve worked with this library in the past and, through recent work, ended up deciding it was high time I put together a small, re-usable utility module.

There were a number of underlying use cases to cover:

  1. The module needed to configure (for example, for searching/sorting) and register multiple tables on a single page.
  2. Tabbed tables needed to be supported.
  3. Due to styling constraints, a custom ‘search’ input needs to be defined and used (so we should prevent Datatables from creating and using its stock search input).
  4. Lastly, we want to override the default sorting iconography; the classic up/down arrow shenanigans.

With this mandate in tow, I set to work!

To start us off, I’ve mocked out two sample pages to illustrate the configurations we need to support, as detailed below:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Multiple Table Setup</title>
    <!--Bootstrap/Fontawesome - included simply to make the sample more 'complete'-->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.7/css/all.css">
    <!--An abstraction available for any additional styling (again, not the primary purpose of this sample, but nice to have on hand)-->
    <link rel="stylesheet" href="css/table-example.css">
    <!--jQuery as required by datatables.js-->
    <script src="https://code.jquery.com/jquery-3.5.0.min.js" integrity="sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=" crossorigin="anonymous"></script>
    <!--Physical datatables.js script reference, which is the library we are adding support to-->
    <script type="text/javascript" src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
    <!--Sample module code on show, along with some JS to register/configure tables-->
    <script type="text/javascript" src="js/datatable-module.js"></script>
    <script type="text/javascript" src="js/multi-tables.js"></script>
</head>
<body>
    <div class="container-fluid">
        <div class="mt-1 mb-2">
            <h1 class="d-inline">Multiple Table Example</h1>
            <a class="btn btn-primary float-right mt-2" href="tabbed-table-setup.html">See Tabbed Example</a>
        </div>
        <div id="FootballFixturesTableContainer" class="table-container">
            <div class="row">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            Football Fixtures
                            <span class="float-right">
                                <input id="SearchFootballFixturesInput" class="form-control" type="text" name="search" placeholder="Search Fixtures" />
                                <i class="fas fa-search search-icon"></i>
                            </span>
                        </div>
                        <div class="card-body">
                            <table id="football-fixtures" class="table">
                                <thead>
                                    <tr>
                                        <th>Fixture Teams<i class="d-none float-right fas"></i></th>
                                        <th class="default-order-column-desc">Date<i class="d-none float-right fas"></i></th>
                                        <th>Officials Selected<i class="d-none float-right fas"></i></th>
                                        <th class="disable-ordering"></th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr>
                                        <td>Norwich City FC vs Manchester United FC</td>
                                        <td data-sort="1599350400">6th September 2020</td>
                                        <td>Yes</td> 
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                        </td>
                                    </tr>
                                    <tr>
                                        <td>Manchester City FC vs Norwich City FC</td>
                                        <td data-sort="1598832000">31st August 2020</td>
                                        <td>No</td> 
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                        </td>
                                    </tr>
                                    <tr>
                                        <td>Arsenal FC vs Liverpool FC</td>
                                        <td data-sort="1599868800">12th September 2020</td>
                                        <td>No</td> 
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                        </td>
                                    </tr>
                                    <tr>
                                        <td>Liverpool FC vs Southampton FC</td>
                                        <td data-sort="1596067200">30th July 2020</td>
                                        <td>Yes</td> 
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                        </td>
                                    </tr>
                                    <tr>
                                        <td>Liverpool FC vs Norwich City FC</td>
                                        <td data-sort="1597449600">15th August 2020</td>
                                        <td>Yes</td> 
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <div id="FoodFestivalsTableContainer" class="table-container mt-2">
            <div class="row">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            Festivals
                            <span class="input-iconised float-right">
                                <input id="SearchFoodFestivalsInput" class="form-control" type="text" name="search" placeholder="Search Festivals" />
                                <i class="fa fa-search search-icon"></i>
                            </span>
                        </div>
                        <div class="card-body">
                            <table id="food-festivals" class="table">
                                <thead>
                                    <tr>
                                        <th class="default-order-column-asc">Festival Name<i class="d-none float-right fas"></i></th>
                                        <th>Date<i class="d-none float-right fas"></i></th>
                                        <th>Theme<i class="d-none float-right fas"></i></th>
                                        <th>Sells Alcohol<i class="d-none float-right fas"></i></th>
                                        <th class="disable-ordering"></th>
                                    </tr>
                                </thead>
                                <tbody>
                                    <tr>
                                        <td>Mulbarton Food Festival</td>
                                        <td data-sort="1618185600">12th April 2021</td>
                                        <td>Vegan Food</td> 
                                        <td>Yes</td>
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Festival</button>
                                        </td>
                                    </tr>
                                    <tr>
                                        <td>Grand Tea Party</td>
                                        <td data-sort="1614556800">1st March 2021</td>
                                        <td>Dainty Sandwiches</td>
                                        <td>No</td> 
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Festival</button>
                                        </td>
                                    </tr>
                                    <tr>
                                        <td>BBQ Delights</td>
                                        <td data-sort="1599868800">12th September 2020</td>
                                        <td>BBQ Food</td>
                                        <td>Yes</td> 
                                        <td>
                                            <button type="button" class="btn btn-primary float-right">Edit Festival</button>
                                        </td>
                                    </tr>
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tabbed Table Setup</title>
    <!--Bootstrap/Fontawesome - included simply to make the sample more 'complete'-->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.7/css/all.css">
    <!--An abstraction available for any additional styling (again, not the primary purpose of this sample, but nice to have on hand)-->
    <link rel="stylesheet" href="css/table-example.css">
    <!--jQuery/Bootstrap JS support (as required by datatables.js and Bootstrap tabs)-->
    <script src="https://code.jquery.com/jquery-3.5.0.min.js" integrity="sha256-xNzN2a4ltkB44Mc/Jz3pT4iU1cmeR0FkXs4pru/JxaQ=" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    <!--Physical datatables.js script reference, which is the library we are adding support to-->
    <script type="text/javascript" src="https://cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js"></script>
    <!--Sample module code on show, along with some JS to register/configure tables-->
    <script type="text/javascript" src="js/datatable-module.js"></script>
    <script type="text/javascript" src="js/tabbed-table-setup.js"></script>
</head>
<body>
    <div class="container-fluid">
        <div class="mt-1 mb-2">
            <h1 class="d-inline">Tabbed Table Example</h1>
            <a class="btn btn-primary float-right mt-2" href="multi-tables.html">See Multi-table Example</a>
        </div>
        <div id="ContentContainer">
            <div class="row">
                <div class="col-12">
                    <div class="card">
                        <div class="card-header">
                            Various Tables
                            <!--Singular search control where the search term itself applies directly to each tabbed table (when switching tabs)-->
                            <span class="float-right">
                                <input id="SearchEntitiesInput" class="form-control" type="text" name="search" placeholder="Search" />
                                <i class="fas fa-search search-icon"></i>
                            </span>
                        </div>
                        <div class="card-body">
                            <!--Tab definitions - each tab contains a table that can searched/sorted individually-->
                            <ul class="nav nav-tabs" id="myTab" role="tablist">
                                <li class="nav-item">
                                    <a class="nav-link active" id="football-fixtures-tab" data-toggle="tab" href="#football-fixtures-tab-content" role="tab" aria-controls="football-fixtures-tab-content" aria-selected="true">Football Fixtures</a>
                                </li>
                                <li class="nav-item">
                                    <a class="nav-link" id="festivals-tab" data-toggle="tab" href="#food-festivals-tab-content" role="tab" aria-controls="food-festivals-tab-content" aria-selected="false">Food Festivals</a>
                                </li>
                                <li class="nav-item">
                                    <a class="nav-link" id="martial-artists-tab" data-toggle="tab" href="#martial-artists-tab-content" role="tab" aria-controls="martial-artists-tab-content" aria-selected="false">Martial Artists</a>
                                </li>
                            </ul>
                            <!--Tabbed content (containing three sample tables registered/configured using datatable-module.js)-->
                            <div class="tab-content" id="myTabContent">
                                <div class="tab-pane fade show active" id="football-fixtures-tab-content" role="tabpanel" aria-labelledby="football-fixtures-tab">
                                    <table id="football-fixtures" class="table">
                                        <thead>
                                            <tr>
                                                <!--Football Fixtures - ordered by Date descending, as specified by the default-order-column-desc class. Ordering is disabled on the last column-->
                                                <th>Fixture Teams<i class="d-none float-right fas"></i></th>
                                                <th class="default-order-column-desc">Date<i class="d-none float-right fas"></i></th>
                                                <th>Officials Selected<i class="d-none float-right fas"></i></th>
                                                <th class="disable-ordering"></th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr>
                                                <td>Norwich City FC vs Manchester United FC</td>
                                                <td data-sort="1599350400">6th September 2020</td>
                                                <td>Yes</td> 
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Manchester City FC vs Norwich City FC</td>
                                                <td data-sort="1598832000">31st August 2020</td>
                                                <td>No</td> 
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Arsenal FC vs Liverpool FC</td>
                                                <td data-sort="1599868800">12th September 2020</td>
                                                <td>No</td> 
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Liverpool FC vs Southampton FC</td>
                                                <td data-sort="1596067200">30th July 2020</td>
                                                <td>Yes</td> 
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Liverpool FC vs Norwich City FC</td>
                                                <td data-sort="1597449600">15th August 2020</td>
                                                <td>Yes</td> 
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Fixture</button>
                                                </td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                                <div class="tab-pane fade" id="food-festivals-tab-content" role="tabpanel" aria-labelledby="food-festivals-tab">
                                    <table id="food-festivals" class="table">
                                        <thead>
                                            <tr>
                                                <!--Food Festivals - ordered by Festival Name ascending, as specified by the default-order-column-asc class. Ordering is disabled on the last column-->
                                                <th class="default-order-column-asc">Festival Name<i class="d-none float-right fas"></i></th>
                                                <th>Date<i class="d-none float-right fas"></i></th>
                                                <th>Theme<i class="d-none float-right fas"></i></th>
                                                <th>Sells Alcohol<i class="d-none float-right fas"></i></th>
                                                <th class="disable-ordering"></th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr>
                                                <td>Mulbarton Food Festival</td>
                                                <td data-sort="1618185600">12th April 2021</td>
                                                <td>Vegan Food</td> 
                                                <td>Yes</td>
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Festival</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Grand Tea Party</td>
                                                <td data-sort="1614556800">1st March 2021</td>
                                                <td>Dainty Sandwiches</td>
                                                <td>No</td> 
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Festival</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>BBQ Delights</td>
                                                <td data-sort="1599868800">12th September 2020</td>
                                                <td>BBQ Food</td>
                                                <td>Yes</td> 
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Festival</button>
                                                </td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                                <div class="tab-pane fade" id="martial-artists-tab-content" role="tabpanel" aria-labelledby="martial-artists-tab">
                                    <table id="martial-artists" class="table">
                                        <thead>
                                            <tr>
                                                <!--Martial Artists - ordered by Name ascending, as specified by the default-order-column-asc class. Ordering is disabled on the last column-->
                                                <th class="default-order-column-asc">Name<i class="d-none float-right fas"></i></th>
                                                <th>Best Trait<i class="d-none float-right fas"></i></th>
                                                <th class="disable-ordering"></th>
                                            </tr>
                                        </thead>
                                        <tbody>
                                            <tr>
                                                <td>Bruce Lee</td>
                                                <td>Everything</td>
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Martial Artist</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Jet Li</td>
                                                <td>Speed</td>
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Martial Artist</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Jackie Chan</td>
                                                <td>Stunts</td>
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Martial Artist</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Cynthia Rothrock</td>
                                                <td>Kicks</td>
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Martial Artist</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Chuck Norris</td>
                                                <td>Memes & Facts</td>
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Martial Artist</button>
                                                </td>
                                            </tr>
                                            <tr>
                                                <td>Tony Jaa</td>
                                                <td>Knees & Elbows</td>
                                                <td>
                                                    <button type="button" class="btn btn-primary float-right">Edit Martial Artist</button>
                                                </td>
                                            </tr>
                                        </tbody>
                                    </table>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

This gives you the bare-bones structure and shows that we are utilising a little Bootstrap for tab support. The other component we are including here is jQuery, which is a key ingredient for our modules ‘underbelly’ (for some reason this brings visions of the original ‘IT’ and the spider scenes to mind…creepy!).

Let’s discuss each component of the module (composed as an IIFE here) in turn; I’ll provide the full code sample at the end for anyone wanting the complete picture in one hit.

With this IIFE I wanted to carefully expose just a couple of functions for registration and configuration of table elements on a given page. To be as explicit as possible tables are registered by their element id:

// Returns just the publically accessible members for use
return {
	configure: configure,
	registerDataTableById: registerDataTableById
}

With the gateway into our module firmly in place let’s investigate what happens on registration/configuration, and, for that matter, how all of this is called.

Each page includes a script to grab hold of and utilise the module, which can be seen in our ‘multi-tables.js’ and ‘tabbed-table-setup.js’ files. After the DOM has been loaded we perform the following:

$(function () {
    // Configure the dataTablesNetTabbedModule (handling stock table setup, options & events). 
    // Then, register tables (using an each here so that for automatic table detection - more robust if Ids change also)
    dataTablesNetTabbedModule.configure(['#SearchFootballFixturesInput', '#SearchFoodFestivalsInput']);
    $('table').each(function () {
        dataTablesNetTabbedModule.registerDataTableById('#' + $(this).attr('id').trim());
    });
});
$(function () {
    // Configure the dataTablesNetTabbedModule (handling stock table setup, options & events). 
    // Then, register tables (using an each here so that for automatic table detection - more robust if Ids change also) - for this tabbed setup we have a single search control
    dataTablesNetTabbedModule.configure(['#SearchEntitiesInput']);
    $('table').each(function () {
        dataTablesNetTabbedModule.registerDataTableById('#' + $(this).attr('id').trim());
    });
});

The configuration element consumes one or more element id’s that denote search inputs. In a tabbed configuration, a single search input acts as a filter for all tables. On the flip-side, if multiple tables exist on a page without a tabbed interface multiple search inputs can be provided, each one intrinsically linked to a table (via the structure of the DOM, as revealed later).

With search inputs, jQuery is used to discover table elements and register each one by id – super simple!

So what lurks behind the configure and registerDataTableById functions? Let’s rip back the wallpaper and have a wee peek!

The configure function is defined as follows:

// Configures the module for use. The 'Search Inputs' passed in are tied to configured events (when search is used/tabs are activated, etc.) and stock table options
// are stored for shared use and consistency
function configure(searchInputIds, sharedTableOptions) {
	// Setup shared table options
	sharedTableOptions = addTableOptionStockDefaults(sharedTableOptions);
	stockTableOptions = sharedTableOptions;

	// Next, configure event listeners
	setupEventListeners(searchInputIds);
}

DataTables allows for quite a bit of customisation and I’ve attempted to allow for this via the inclusion of a ‘sharedTableOptions’ argument, which the caller can provide if they wish (this comprises of a simple object). If an object is not provided then I put in place some stock, default options. Then, each table has supported event listeners configured. For example, when keystrokes occur in the search input or a table header is clicked to sort the data.

As far as default options the core thing I wanted control over was the messaging that occurs when a table contains no data, as shown in this little snippet:

// Utility function that sets up stock table options (if overrides are provided they are left alone). A new object is created, if the one passed in undefined
function addTableOptionStockDefaults(sharedTableOptions) {
	sharedTableOptions = (typeof sharedTableOptions === 'undefined') ? {} : sharedTableOptions;

	return $.extend(sharedTableOptions, {
		"paging": false,
		"info": false,
		// Zero record message is difficult to alter/style so configuring directly is the easiest option
		"language": {
			"zeroRecords": '<span style="display:block; text-align:center; padding: 0.8rem 0 1.2rem 0;">No matching records found</span>'
		}
	});
}

The setting up of event listeners is slightly more complex and contains logic to detect if we are dealing with a tabbed configuration:

// Sets up stock event listeners (fixed currently to the known template structure - we can make it more flexible if required) - triggers 
// for all tables registered within this module
function setupEventListeners(searchInputIds) {
	if (searchInputIds && searchInputIds.length > 0) {
		// An unexpected use case, currently, is to have multiple search inputs with a tabbed interface. This will require (likely) further work to sure up
		let tabsPresent = $('.nav-tabs').length > 0;
		if (searchInputIds.length > 1 && tabsPresent) {
			throw 'A tabbed setup with multiple search inputs represents an untested configuration. Please inspect the code, implement and test thoroughly';
		}

		searchInputIds.forEach(searchInputId =>
		{
			if ($(searchInputId).length > 0) {
				// Setup events only if the searchInputId is configured/discovered
				$('.container-fluid').on('keyup', searchInputId, function () {
					searchActiveTable($(this))
				});

				// Only perform the event hook if '.nav-tabs' exist. If a more complicated page configuration is needed we will only want events
				// to hook up/fire (trigger) when the tab is tied to a table. Overly complex to cater for that at this time, however
				if (tabsPresent) {
					$('.container-fluid').on('shown.bs.tab', 'a[data-toggle="tab"]', function () {
						searchActiveTable($(searchInputId));
					});
				}

				// Handle UI alterations on header click (up/down fractal arrows, based on asc/desc ordering)
				$('.container-fluid').on('click', 'th', function () {
					handleTableHeaderUI($(this));
				});

				registeredSearchInputCount++;
			}
		});
	}
}

At this time the module is not perfectly geared up to deal with tabbed configurations running in tandem with other, non-tabbed, tables in play on a single page. You’ll note that an error is thrown in this instance for clarity, which I think is an OK compromise in the short-term. For Bootstrap-based tabs, ‘shown.bs.tab’ is used to underpin direct searching of a table (on tab switch) – this code can be freely modified and is probably a candidate for further configuration options being passed in future. Searching is also triggered on ‘keyup’, with the active table being inferred within the ‘searchActiveTable’ function. Lastly, you’ll see that the ‘handleTableHeaderUI’ function is called when a header element is clicked, for sorting. There is most certainly room for improvement here, as I am blurring the lines between search inputs and table-based events here; it could definitely use some further tidying up.

Logic defined inside the ‘searchActiveTable’ function requires an awareness of how many search inputs are lingering in the wilderness of the page (for accurate table discovery). We’ll see that in motion shortly.

On to the next public entry point in the module – the ‘registerDataTableById’ function:

// Registers a table, by id, within the module (the element must exist and must be a 'table' - module table options must also be available)
function registerDataTableById(elementId) {
	if (elementId && stockTableOptions) {
		var discoveredTable = $(elementId);

		if (discoveredTable && discoveredTable.prop('tagName') === "TABLE") {
			var defaultOrderingColumn = getDefaultOrderingColumnFromTable(discoveredTable);
				specificTableOptions = setupTableSpecificOptions(discoveredTable, defaultOrderingColumn);

			// Add the configured datatable to the module array for future use and hide generated elements (can be expanded, as needed - show/hide should ideally 
			// be based on options = future enhancement)
			currentTables.push(discoveredTable.DataTable(specificTableOptions));
			hideDataTablesGeneratedElements();
			handleTableHeaderUI(defaultOrderingColumn);
		}
	}
}

First, we go through a little discovery process to ensure the element id provided is a) valid and exists and b) is tied to a table element. After obtaining a jQuery element for the table, by id, the stock table generation options and ordering settings are defined and applied.

Let’s inspect the ‘getDefaultOrderingColumnFromTable’ function next. The HTML example files illustrate this in use, so feel free to refer to them for extra detail. In essence, default column ordering can easily be applied by placing a CSS class on a ‘th’ element with the prefix of ‘default-order-column-‘. After the final hyphen, the term ‘desc’ can be added to force an ‘order by descending’ setup; any other term will enforce an ‘order by ascending’ configuration. If no class is discovered on any ‘th’ element we simply order by the first column, ascending – lovely and simple.

// Returns the default column order table header, from the target table, if available
function getDefaultOrderingColumnFromTable(discoveredTable) {
	var defaultOrderingColumn = discoveredTable.find("[class^='default-order-column-']");

	// Return the first 'th' if no specific default has been applied
	return defaultOrderingColumn.length === 0 ? discoveredTable.find('th').first() : defaultOrderingColumn;
}

More detailed table setup is then spun into motion via a call to the ‘setupTableSpecificOptions’ function:

// Private function for configuring the specific configurations on a table, based on stock options, on registration
function setupTableSpecificOptions(discoveredTable, defaultOrderingColumn) {
	// Prepare a local, overridable copy of table options (for modification, if needed, but still based on stock settings)
	var specificTableOptions = $.extend({}, {}, stockTableOptions);

	// 1) Define those columns for which ordering is disabled
	var disabledOrderingColumnIndexArray = [];
	$(discoveredTable).find('th.disable-ordering').each(function () {
		disabledOrderingColumnIndexArray.push($(this).index());
	});

	// 2) Define a column to act as the default 'ordering' column (the class applied denotes this column and whether we order asc/desc)
	var defaultOrderingColumnIndex = defaultOrderingColumn.length !== 0 ? defaultOrderingColumn.index() : 0,
		defaultOrderType = defaultOrderingColumn.hasClass('default-order-column-desc') ? 'desc' : 'asc';

	// 3) Modify and return specific options as prepared in this function
	specificTableOptions = $.extend({}, {
		"columnDefs": [{
			"orderable": false,
			"targets": disabledOrderingColumnIndexArray
		}],
		"order": [[defaultOrderingColumnIndex, defaultOrderType]]
	}, specificTableOptions);

	return specificTableOptions;
}

From an extensibility standpoint, to make this as flexible as possible, we use ‘$.extend’ to provide an overridable (per table) version of the stock table options. On table creation, it could well be the case that certain columns should not allow ordering. This can be achieved by applying the ‘disable-ordering’ CSS class to any given ‘th’ element. The default order column (and ordering type) is then readied and the table-specific options are returned to the calling function.

We are at this point ready to initialise a DataTables ‘data table’ for the table element in scope. This is achieved via a call to the ‘DataTable’ function – created objects are stored within the module for future reference during searching/ordering, etc.

DataTables does create, on the fly, a search input for any table where ‘Datatable’ is called. This is actually something I want to avoid in my case, and is where the ‘hideDataTablesGeneratedElements’ comes into the fold:

// Utility function (which can be added) that hides unrequired elements after a datatable is generated (using the tableElement.DataTable() call)
function hideDataTablesGeneratedElements() {
	$('.dataTables_filter').hide();
}

This simply hides the element, for the time being (I couldn’t see a specific option to control this, so this is the resolution I went with).

Table headers include some iconography based on sorting, and this code is very much rigged for Bootstrap. This, of course, forms another excellent candidate for further flexibility being introduced. The basic intent here is to show up/down chevron icon based on how sorting is configured:

// Configures the table header sortable icons (UI work essentially) based on the clicked table header element (and current 'sortable' state) 
function handleTableHeaderUI(tableHeaderElementInScope) {
	if (tableHeaderElementInScope
		&& tableHeaderElementInScope.length > 0
		// If sorting is disabled for this column by datatables.net do not proceed
		&& !tableHeaderElementInScope.hasClass('sorting_disabled')) {
		// The element provided is set...next hide icons for any sibling th elements (just for this table) and show the relevant up/down arrow
		// based on the current sortable state of the row (add/remove internally check for the presence of the classes specified with no side effects).
		// Always cleaner to wipe down all classes and reapply just whats needed to avoid getting into an undesired state
		tableHeaderElementInScope.siblings('th').find('i').addClass('d-none');
		tableHeaderElementInScope.find('i')
			.removeClass('d-none fa-chevron-down fa-chevron-up')
			.addClass(tableHeaderElementInScope.hasClass('sorting_asc') ? 'fa-chevron-down' : 'fa-chevron-up');
	}
}

This is called on startup and on every subsequent click of a table header.

That’s it from the configuration and registration side of things.

So, what triggers a ‘search’? When a tabbed setup is involved a search is triggered on tab change. Also, searching is actively triggered on ‘keyup’ within a search input. As we illustrated before, these events call the ‘searchActiveTable’ function. All of the work for inferring the active table is done inside this function:

// Represents the core search function (table content filtered based on the input value from the configured search input, based on the events handled/setup within the module)
function searchActiveTable(currentSearchInput) {
	var visibleActiveTableId;
	if (registeredSearchInputCount > 1) {
		// Multiple search inputs mean we discover the table via location (relative to the search input). More complex use cases can be added, as needed
		visibleActiveTableId = currentSearchInput.closest('.table-container').find('table:visible').attr('id');
	}
	else {
		// Obtain the active 'tab' data-tab value (or just the singular table) - this is used to infer the active data table
		visibleActiveTableId = $('table:visible').attr('id');
	}

	// From the active table, infer the active table stored within the module (has to be registered) and, if found, filter the results in the table based on the supplied search term
	if (visibleActiveTableId) {
		var activeTable = currentTables.find(function (table) {
			return table.table().node().id === visibleActiveTableId
		});

		if (activeTable) {
			activeTable.search(currentSearchInput.val()).draw();
		}
	}
}

In this situation, the only requirement whereby multiple tables exist on a page in a non-tabbed setup (from a developer setup perspective) is that every table must have a wrapper with the ‘table-container’ class (a div, for example). The provided HTML samples illustrate this. Based on the search input being acted upon this underpins how table discovery works via ‘closest().find()’. Otherwise, in other setups, only a single table is expected to be visible. We ultimately want to extract the id of the table element, to initiate a search.

The active table is found via the ‘currentTables’ array stored at the module level, which you can find referenced inside the ‘registerDataTableById’. From a DataTables perspective performing a search/re-render of information is amazingly simple:

activeTable.search(currentSearchInput.val()).draw();

For reference, here is the full module code:

// Utility module for handling stock configuration/setups for datatables
var dataTablesNetTabbedModule = (function () {
    // Table discovery occurs (for searching) in a slightly different manner depending on the registered search input count
    let registeredSearchInputCount = 0;

    // Fields (stock table options and an array to store registered tables)
    var stockTableOptions,
        currentTables = [];

    // Private Functions

    // Utility function that sets up stock table options (if overrides are provided they are left alone). A new object is created, if the one passed in undefined
    function addTableOptionStockDefaults(sharedTableOptions) {
        sharedTableOptions = (typeof sharedTableOptions === 'undefined') ? {} : sharedTableOptions;

        return $.extend(sharedTableOptions, {
            "paging": false,
            "info": false,
            // Zero record message is difficult to alter/style so configuring directly is the easiest option
            "language": {
                "zeroRecords": '<span style="display:block; text-align:center; padding: 0.8rem 0 1.2rem 0;">No matching records found</span>'
            }
        });
    }

    // Utility function (which can be added) that hides unrequired elements after a datatable is generated (using the tableElement.DataTable() call)
    function hideDataTablesGeneratedElements() {
        $('.dataTables_filter').hide();
    }

    // Sets up stock event listeners (fixed currently to the known template structure - we can make it more flexible if required) - triggers 
    // for all tables registered within this module
    function setupEventListeners(searchInputIds) {
        if (searchInputIds && searchInputIds.length > 0) {
            // An unexpected use case, currently, is to have multiple search inputs with a tabbed interface. This will require (likely) further work to sure up
            let tabsPresent = $('.nav-tabs').length > 0;
            if (searchInputIds.length > 1 && tabsPresent) {
                throw 'A tabbed setup with multiple search inputs represents an untested configuration. Please inspect the code, implement and test thoroughly';
            }

            searchInputIds.forEach(searchInputId =>
            {
                if ($(searchInputId).length > 0) {
                    // Setup events only if the searchInputId is configured/discovered
                    $('.container-fluid').on('keyup', searchInputId, function () {
                        searchActiveTable($(this))
                    });

                    // Only perform the event hook if '.nav-tabs' exist. If a more complicated page configuration is needed we will only want events
                    // to hook up/fire (trigger) when the tab is tied to a table. Overly complex to cater for that at this time, however
                    if (tabsPresent) {
                        $('.container-fluid').on('shown.bs.tab', 'a[data-toggle="tab"]', function () {
                            searchActiveTable($(searchInputId));
                        });
                    }

                    // Handle UI alterations on header click (up/down fractal arrows, based on asc/desc ordering)
                    $('.container-fluid').on('click', 'th', function () {
                        handleTableHeaderUI($(this));
                    });

                    registeredSearchInputCount++;
                }
            });
        }
    }

    // Returns the default column order table header, from the target table, if available
    function getDefaultOrderingColumnFromTable(discoveredTable) {
        var defaultOrderingColumn = discoveredTable.find("[class^='default-order-column-']");

        // Return the first 'th' if no specific default has been applied
        return defaultOrderingColumn.length === 0 ? discoveredTable.find('th').first() : defaultOrderingColumn;
    }

    // Private function for configuring the specific configurations on a table, based on stock options, on registration
    function setupTableSpecificOptions(discoveredTable, defaultOrderingColumn) {
        // Prepare a local, overridable copy of table options (for modification, if needed, but still based on stock settings)
        var specificTableOptions = $.extend({}, {}, stockTableOptions);

        // 1) Define those columns for which ordering is disabled
        var disabledOrderingColumnIndexArray = [];
        $(discoveredTable).find('th.disable-ordering').each(function () {
            disabledOrderingColumnIndexArray.push($(this).index());
        });

        // 2) Define a column to act as the default 'ordering' column (the class applied denotes this column and whether we order asc/desc)
        var defaultOrderingColumnIndex = defaultOrderingColumn.length !== 0 ? defaultOrderingColumn.index() : 0,
            defaultOrderType = defaultOrderingColumn.hasClass('default-order-column-desc') ? 'desc' : 'asc';

        // 3) Modify and return specific options as prepared in this function
        specificTableOptions = $.extend({}, {
            "columnDefs": [{
                "orderable": false,
                "targets": disabledOrderingColumnIndexArray
            }],
            "order": [[defaultOrderingColumnIndex, defaultOrderType]]
        }, specificTableOptions);

        return specificTableOptions;
    }

    // Configures the table header sortable icons (UI work essentially) based on the clicked table header element (and current 'sortable' state) 
    function handleTableHeaderUI(tableHeaderElementInScope) {
        if (tableHeaderElementInScope
            && tableHeaderElementInScope.length > 0
            // If sorting is disabled for this column by datatables.net do not proceed
            && !tableHeaderElementInScope.hasClass('sorting_disabled')) {
            // The element provided is set...next hide icons for any sibling th elements (just for this table) and show the relevant up/down arrow
            // based on the current sortable state of the row (add/remove internally check for the presence of the classes specified with no side effects).
            // Always cleaner to wipe down all classes and reapply just whats needed to avoid getting into an undesired state
            tableHeaderElementInScope.siblings('th').find('i').addClass('d-none');
            tableHeaderElementInScope.find('i')
                .removeClass('d-none fa-chevron-down fa-chevron-up')
                .addClass(tableHeaderElementInScope.hasClass('sorting_asc') ? 'fa-chevron-down' : 'fa-chevron-up');
        }
    }

    // Represents the core search function (table content filtered based on the input value from the configured search input, based on the events handled/setup within the module)
    function searchActiveTable(currentSearchInput) {
        var visibleActiveTableId;
        if (registeredSearchInputCount > 1) {
            // Multiple search inputs mean we discover the table via location (relative to the search input). More complex use cases can be added, as needed
            visibleActiveTableId = currentSearchInput.closest('.table-container').find('table:visible').attr('id');
        }
        else {
            // Obtain the active 'tab' data-tab value (or just the singular table) - this is used to infer the active data table
            visibleActiveTableId = $('table:visible').attr('id');
        }

        // From the active table, infer the active table stored within the module (has to be registered) and, if found, filter the results in the table based on the supplied search term
        if (visibleActiveTableId) {
            var activeTable = currentTables.find(function (table) {
                return table.table().node().id === visibleActiveTableId
            });

            if (activeTable) {
                activeTable.search(currentSearchInput.val()).draw();
            }
        }
    }

    // Publically Accessible Members

    // Configures the module for use. The 'Search Inputs' passed in are tied to configured events (when search is used/tabs are activated, etc.) and stock table options
    // are stored for shared use and consistency
    function configure(searchInputIds, sharedTableOptions) {
        // Setup shared table options
        sharedTableOptions = addTableOptionStockDefaults(sharedTableOptions);
        stockTableOptions = sharedTableOptions;

        // Next, configure event listeners
        setupEventListeners(searchInputIds);
    }

    // Registers a table, by id, within the module (the element must exist and must be a 'table' - module table options must also be available)
    function registerDataTableById(elementId) {
        if (elementId && stockTableOptions) {
            var discoveredTable = $(elementId);

            if (discoveredTable && discoveredTable.prop('tagName') === "TABLE") {
                var defaultOrderingColumn = getDefaultOrderingColumnFromTable(discoveredTable);
                    specificTableOptions = setupTableSpecificOptions(discoveredTable, defaultOrderingColumn);

                // Add the configured datatable to the module array for future use and hide generated elements (can be expanded, as needed - show/hide should ideally 
                // be based on options = future enhancement)
                currentTables.push(discoveredTable.DataTable(specificTableOptions));
                hideDataTablesGeneratedElements();
                handleTableHeaderUI(defaultOrderingColumn);
            }
        }
    }

    // Returns just the publically accessible members for use
    return {
        configure: configure,
        registerDataTableById: registerDataTableById
    }
})();

That’s basically it! A simple, but useful out of the box construct for quickly getting up and running with DataTables. There are far more options that could be incorporated too. Here’s a couple of screenshots to give you a rough idea of how the tables look post-construction:

Multi Table Example
Multi Table Example.
Tabbed Table Example
Tabbed Table Example.

I hope you enjoyed this run through. I still have a little conversion work to do to bring in ES6+ language features fully, so very much a work in progress. The code can be found on GitHub here if anyone fancies tinkering with it.

Until the next time, happy coding!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.