Example Modules
Overview
It can be tricky to know where to start when you are setting out to build your own table module. This section aims to take you through some practical examples to help you understand how modules work.
For full documentation on all the features available to module builders, checkout the Building Modules Documentation
Selectable Columns Module
In this example we will build out a simple column selection module that visually selects a column when someone clicks on the column header.
In this module we will use the following techniques:
- Registering options on the column definition
- Registering funtions on the external column component
- Storing data on the internal column component
- Manipulating DOM elements on the cell and column components
- Subscribing to internal table events
- Dispatching internal table events
- Dispatching external table events
If you would like to see the completed code for the module, instead of the step by step instructions, skip ahead to the Complete Module Code
Structure And Registration
We start be defining our SelectableColumnsModul which will extend the Module class imported from the Tabulator library
import {Tabulator, Module} from 'tabulator-tables'; class SelectableColumnsModule extends Module{ constructor(table){ super(table); } initialize(){ //called by the table when it is ready for module integrations } } SelectableColumnsModule.moduleName = "selectableColumns";
After this we will need to register our new module with Tabulator so it can be included with new tables. To do this, call the registerModule function, passing in the SelectableColumnsModule class:
Tabulator.registerModule(SelectableColumnsModule);
Column Option Registration
Next up we need to register the column options that our users will use to configure this module in the table definition (and their default values). We do this using the registerColumnOption function in the table constructor.
In this module we will be registering one option, selectable that allow selection of that column when the user clicks the column header, which will default to false.
constructor(table){ super(table); //register column definition options this.registerColumnOption("selectable", false); }
These can then be configured by the user per column in the table constructor:
var table = new Tabulator("#example-table", { columns:[ {title:"Name", field:"name", selectable:true} ] });
Or can it can be set globaly for all columns in the columnDefaults option
var table = new Tabulator("#example-table", { columnDefaults:{ selectable:true, } });
Event Subscribers
In order to manage the selection of columns we will need to subscribe to a couple of events on the table. We can do this using the subscribe function inside of the module initialize function.
Column Initialization
When columns are first added to the table, there is an initialization phase that allow modules to register their interest in the column. We will bind to this column-init event and use it to setup our modules meta data object on the column components modules object.
initialize(){ this.subscribe("column-init", this.initializeColumn.bind(this)); } //initialize selectable column initializeColumn(column){ //check if column has selectable enabled before initializing if(column.definition.selectable){ column.modules.selectable = { selected:false, }; } }
We make sure that a column has our selectable option enabled in its definition before initializing it
Click Event Handling
The next event we need to subscribe to is the column-click event, this will tell us when someone has clicked in a column header.
In this event subscriber we will first check to see if the column was initialized as selectable by checking for the selectable property on the columns modules object
We will then toggle the current selected state of the column by inverting the selectable property.
Then depending on the selectable state of the column we will then either add or remove the col-selected class from the columns header and cell elements./p>
initialize(){ ... this.subscribe("column-click", this.toggleSelection.bind(this)); } //toggle selection on cell click toggleSelection(e, column){ var colEl, colCells; //only perform action if column is selectable if(column.modules.selectable){ column.modules.selectable.selected = !column.modules.selectable.selected; colEl = column.getElement(); //get column header element colCells = column.getCells(); //get cell components for column if(column.modules.selectable.selected){ colEl.classList.add("col-selected"); colCells.forEach((cell) => { cell.getElement().classList.add("col-selected"); }); }else{ colEl.classList.remove("col-selected"); colCells.forEach((cell) => { cell.getElement().classList.remove("col-selected"); }); } } }
Event Dispatching
We then want to let other modules and users know that something has changed in the table. we do this using events.
Internal Events
If you want other modules to be able to take action based off of your module, you can dispatch internal events that other modules can subscribe to.
In this case we will update our toggleSelection function above to dispatch column-selected and column-deselected events after the class list has been updated
toggleSelection(e, column){ ... if(column.modules.selectable.selected){ ... this.dispatch("column-selected", column); }else{ ... this.dispatch("column-deselected", column); } }
You should make sure that the events you are dispatching do not conflict with any of the existing built in event names, otherwise it may cause the table to malfunction. A full list of these events can be found on the Internal Event Bus Documentation
External Events
If you want to let the user know that something has changed, you can dispatch external events that can subscribed to outside the table.
In this case we will update our toggleSelection function above to dispatch columnSelected and columnDeselected external events after the class list has been updated
Because these events are fired outside the table, it is important that we provide the external Column Component to the event, and not the internal component. To do this we call the getComponent function on the column, this returns its external component.
toggleSelection(e, column){ ... if(column.modules.selectable.selected){ ... this.dispatchExternal("columnSelected", column.getComponent()); }else{ ... this.dispatchExternal("columnDeselected", column.getComponent()); } }
You should make sure that the events you are dispatching do not conflict with any of the existing built in external event names, otherwise it may cause the table to malfunction. A full list of these events can be found on the External Event Documentation
Component Function
We also want to provide the user with a way to check if a column is selected. We can do this by registering a component function in the module construtor using the registerComponentFunction function.
In this case we want to register the function on the column component, so we pass a string of column into the first argument. The second argument is the name we want the function to have, and the third argument is the function to be called.
When binding a function to a component, the first argument passed into the function will always be the component the function was called on, followed by any arguments passed to the function.
constructor(table){ super(table); //register column definition options this.registerComponentFunction("column", "isSelected", this.selectedCheck.bind(this)); } selectedCheck(column){ return !!(column.modules.selectable && column.modules.selectable.selected); }
In this case we want to return true if our column was selected. so we first check that the column was initialized with the selectable option by checking to see if the selectable object is set on the columns module object, and then if it is, check if the selected option is set to true.
This function can then be called on any external column component, for example we could check if the first column in the table was selected by retreivieg the first column with the getColumns function on the table and the call our isSelected function on that:
var firstColumnSelected = table.getColumns()[0].isSelected();
Complete Module Code
So if we put all of that together we get the following module code:
import {Tabulator, Module} from 'tabulator-tables'; class SelectableColumnsModule extends Module{ constructor(table){ super(table); //register column definition options this.registerColumnOption("selectable", false); //register column component functions this.registerComponentFunction("column", "isSelected", this.selectedCheck.bind(this)); } //called by the table when it is ready for module interactions initialize(){ this.subscribe("column-init", this.initializeColumn.bind(this)); this.subscribe("column-click", this.toggleSelection.bind(this)); } //initialize selectable column initializeColumn(column){ if(column.definition.selectable){ column.modules.selectable = { selected:false, }; } } //toggle selection on cell click toggleSelection(e, column){ var colEl, colCells; //only perform action if column is selectable if(column.modules.selectable){ column.modules.selectable.selected = !column.modules.selectable.selected; colEl = column.getElement(); //get column header element colCells = column.getCells(); //get cell components for column if(column.modules.selectable.selected){ colEl.classList.add("col-selected"); colCells.forEach((cell) => { cell.getElement().classList.add("col-selected"); }); //dispatch events this.dispatch("column-selected", column); this.dispatchExternal("columnSelected", column.getComponent()); }else{ colEl.classList.remove("col-selected"); colCells.forEach((cell) => { cell.getElement().classList.remove("col-selected"); }); //dispatch events this.dispatch("column-deselected", column); this.dispatchExternal("columnDeselected", column.getComponent()); } } } //check if cell is selected selectedCheck(column){ return !!(column.modules.selectable && column.modules.selectable.selected); } } SelectableColumnsModule.moduleName = "selectableColumns"; Tabulator.registerModule(SelectableColumnsModule);
Advert Module
In this example we will build a component that inserts a full width banner advert into the table every, X number of rows. While this may be an undesireable feature in real life, it proivdes a simple example into how to add features to the row management pipeline
In this module we will use the following techniques:
- Registering options on the table
- Registering functions on the table
- Registering a display handler
- Triggering a data refresh
If you would like to see the completed code for the module, instead of the step by step instructions, skip ahead to the Complete Module Code
Structure And Registration
We start be defining our AdvertModule which will extend the Module class imported from the Tabulator library
import {Tabulator, Module} from 'tabulator-tables'; class AdvertModule extends Module{ constructor(table){ super(table); } initialize(){ //called by the table when it is ready for module integrations } } AdvertModule.moduleName = "advert";
After this we will need to register our new module with Tabulator so it can be included with new tables. To do this, call the registerModule function, passing in the AdvertModule class:
Tabulator.registerModule(AdvertModule);
Option Registration
Next up we need to register the options that our users will use to configure this module in the table definition (and their default values). We do this using the registerTableOption function in the table constructor.
constructor(table){ super(table); //register table options this.registerTableOption("adverts", false); this.registerTableOption("advertInterval", 5); this.registerTableOption("advertSrc", ""); }
In this case we are registering three options:
- adverts - this will be used to enable or disable the module (modules should generally be disabled until a user enables them to reduce the processing load on the table)
- advertInterval - how many rows should appear between each advert
- advertSrc - the src url for the advert image
These can then be configured by the user in the table constructor:
var table = new Tabulator("#example-table", { adverts:true, advertInterval:5, advertSrc:"./advert.jpg", });
Display Handler
We then need to register our display handler function, this will process the tables rows and insert the adverts.
Register The Handler
We do this by calling the registerDataHandler function inside the initialize function. For the sake of clean code, we are then going to pass in the dataHandler function as the handler.
We are going to set the priority of this handler to 60, making it the last handler in the row management pipeline. this will mean that every other row management option will have been processed first, so any child rows or group headers will have already been inserted. If for example you didnt want to break up a rows children with adverts you could give the handler a priority of 25 which would insert the adverts after rows had been grouped but before child rows were added
initialize(){ //check to see if module has been enabled beofre regitering handlers if(this.options("adverts")){ //register data handler after page handler this.registerDataHandler(this.dataHandler.bind(this), 60); } } //define data handler function dataHandler(rows){ //handle the rows }
It is important to note that we are checking to see if the adverts option has been set before we register the data handler. That way if the module isnt being used it doesnt clutter up the row management pipeline with unnecessary logic.
Manipulate The Row Array
The handler function will be passed an array of rows from the previous handler as its first argument. The handler should manipulte this array and then return the resulting array.
In this case we will use the advertInterval option to insert our advert every X rows.
dataHandler(rows){ var interval = this.options("advertInterval"), position = interval; //insert adverts every interval while (position < rows.length + 1) { rows.splice(position, 0, this.advertRow()); position += interval + 1; } return rows; }
The advertRow function generates the element to be inserted and will be discussed in the next step.
Pseudo Row Component
If you are going to be inserting anything into the rows array, it must either be an internal row component, or an object that has a similar interface that the renderer can understand.
To help with scenarios like this where we are looking to insert a non standard row into the aray (ie a row that doesn't contain cells), Tabulator provides the PseudoRow class that generates an empty row DOM element and can be sensibly handled by the renderer
This can be imported to your project from the Tabulator library along with the Module class
import {Tabulator, Module, PseudoRow} from 'tabulator-tables';
The advertRow function will create a new pseudo row component, retrieve the row DOM element and then append the advert to it using the advertSrc table option for the image url.
When instatiating the PsudoRow class you should pass in a string representing the type of pseudo row this is. This can be any freetext value you like, and by convention is just the lowercase name of the module.
advertRow(){ var row, el, ad; //create pseudo row component row = new PseudoRow("advert"); //get containing element for row el = row.getElement(); el.classList.add("row-advert"); //create advert contents ad = document.createElement("img"); ad.src = this.options("advertSrc"); ad.style.width = '100%'; ad.style.display = 'block'; //append contents to row el.appendChild(ad); return row; }
Table Functions
In this module we are also going to allow the user to change the image displayed in the advert using a setAdvert function.
table.setAdvert("./advert2.jpg");
Function Registration
To do this we will need to register a function on the table using the registerTableFunction function, For the sake of clean code, we are then going to pass in the setAdvert function as the handler.
constructor(table){ ... //register table functions this.registerTableFunction("setAdvert", this.setAdvert.bind(this)); } setAdvert(src){ //handle table function }
Update Table Option
The setAdvert function will accept the new advert url as its first argument, and then update the advertSrc table option using the setOption function, passing in the option as the first argument and the new value as its second option.
setAdvert(src){ this.setOption("advertSrc", src); }
Data Refresh
It will then need to refresh the table data because the new image will need a redaw of the table to be displayed. To do this it will call the refreshData function. To make the operation as smooth as possible we can keep the table in the same vertical scroll position by passing true into the first argument of this function. If we left this out the table would scroll to the top when the data was refreshed.
setAdvert(src){ this.setOption("advertSrc", src); this.refreshData(true); }
Pipleine Efficiency
One of the major benefits of the display pipeline is that it only redraws data that has changed, by triggering the refreshData function on this module it means only this modules handler and handlers of a higher pririty will be re-run
Complete Module Code
So if we put all of that together we get the following module code:
import {Tabulator, Module, PseudoRow} from 'tabulator-tables'; class AdvertModule extends Module{ constructor(table){ super(table); //register table options this.registerTableOption("adverts", false); this.registerTableOption("advertInterval", 5); this.registerTableOption("advertSrc", ""); //register table functions this.registerTableFunction("setAdvert", this.setAdvert.bind(this)); } //called by the table when it is ready for module integrations initialize(){ //check to see if module has been enabled beofre regitering handlers if(this.options("adverts")){ //register data handler after page handler this.registerDataHandler(this.dataHandler.bind(this), 60); } } //define data handler function dataHandler(rows){ var interval = this.options("advertInterval"), position = interval; //insert adverts every interval while (position < rows.length + 1) { rows.splice(position, 0, this.advertRow()); position += interval + 1; } return rows; } //create internal row component for the row advertRow(){ var row, el, ad; //create pseudo row component row = new PseudoRow("advert"); //get containing element for row el = row.getElement(); el.classList.add("row-advert"); //create advert contents ad = document.createElement("img"); ad.src = this.options("advertSrc"); ad.style.width = '100%'; ad.style.display = 'block'; //append contents to row el.appendChild(ad); return row; } //change advert after the table has been drawn setAdvert(src){ this.setOption("advertSrc", src); this.refreshData(true); } } AdvertModule.moduleName = "advert"; Tabulator.registerModule(AdvertModule);
Database Module
In this example we will build a module that requests data from a JavaScript based database API. it will incoporate parameters from other modules into its request.
In this module we will use the following techniques:
- Registering options on the table
- Conditionally Subscribing to internal table events
- Handling request parameters
- Handling the data-load internal event
If you would like to see the completed code for the module, instead of the step by step instructions, skip ahead to the Complete Module Code
Database Library
In this example we will be using a simplified fictional database library to make our remote requests. The important lessons to take away from this example are how you can use a module to hook into the data loading events to provide the table with remote data.
In this example we will make a few assumptions about our fictional database library to keep our code clean.
That you start a query by defining the table on the database that you want to access:
var query = db.table("table_name");
That the query will chain methods for sorting and filtering data:
var query = db.table("table_name").filter("age", ">", 22).sort("name", "asc");
That to run a query you call the run function at the end of your method chain:
var results = db.table("table_name").filter("age", ">", 22).sort("name", "asc").run();
And that the run function returns a promise that resolves with an array of data objects that are compatible with the Tabulator setData function.
Structure And Registration
We start be defining our DBModule which will extend the Module class imported from the Tabulator library
We will add a couple of internal variable to hold the database object and table name after these have been initialized by the user in the table options.
import {Tabulator, Module} from 'tabulator-tables'; class DBModule extends Module{ constructor(table){ super(table); this.db = null; //hold the database this.dbTable = null; //hold the database table name } initialize(){ //called by the table when it is ready for module integrations } } DBModule.moduleName = "db";
After this we will need to register our new module with Tabulator so it can be included with new tables. To do this, call the registerModule function, passing in the DBModule class:
Tabulator.registerModule(DBModule);
Option Registration
Next up we need to register the options that our users will use to configure this module in the table definition (and their default values). We do this using the registerTableOption function in the table constructor.
constructor(table){ super(table); this.registerTableOption("database", false); this.registerTableOption("databaseTable", ""); }
In this case we are registering three options:
- database - this will hold the database object
- databaseTable - the table name to be queried
These can then be configured by the user in the table constructor:
var table = new Tabulator("#example-table", { database:new Database(), databaseTable:"example", });
Event Subscribers
In order to tap into the data loading pipeline we need to subscribe to a couple of events using subscribe function inside of the module initialize function.
initialize(){ //check to see if module has been enabled before subscribing to events if(this.options("database")){ //copy table options to local variables for ease of access this.db = this.options("database"); this.dbTable = this.options("databaseTable"); //subscribe to events this.subscribe("data-loading", this.requestDataCheck.bind(this)); this.subscribe("data-load", this.requestData.bind(this)); } }
In the initialize function we check first to see if our database options has been defined by the user. if not we do not add any events listers as the user is not using our module.
We also assign a couple of the table options to local variables to make the code cleaner.
Data Loading
When the table is instructed to load data, the data-loading event is dispatched. This is a check type event and is essentially asking any modules if they would like to override the build in data loading and provide data themselves.
This function receives several argument that outline the details of a request and can be used by a module to decide whether it wants to handle the request or not.
If a module want to handle the request it should return true. In this case we want to handle the request if both the database and table have been defined by the user.
initialize(){ if(this.options("database")){ ... this.subscribe("data-loading", this.requestDataCheck.bind(this)); } } //request a remote data load if all the table options were setup correctly requestDataCheck(data, params, config, silent){ return !!(this.db && this.dbTable); }
Making the request
When the table is ready to make the request it will dispatch the data-load event. Your subscriber to this event should make the request to the database and then return a promise that resolves with the data as a an array of Row Data
Which in this example we are assuming that the run function on our query chain will return a promise as needed.
initialize(){ if(this.options("database")){ ... this.subscribe("data-load", this.requestData.bind(this)); } } //request a remote data load if all the table options were setup correctly requestData(data, params){ var query = this.db.table(this.dbTable); return query.run(); }
Request Parameters
If our database library allows it, we can also use parameters passed into the request from other modules to alter our query.
As part of the data loading lifecycle a data-params event is dispatched that allows other libraries to register params they would like included in the request.
For example when the sortMode or filterMode options are set to remote those modules will add parameters to requests, containing an array of sorters/filters, to allow the remote request to handle sorting or filtering instead of their respective modules.
var table = new Tabulator("#example-table", { sortMode:"remote", filterMode:"remote", })
In this case we can update our requestData to handle the params, which are passed into the functions second argument.
//build database query, and return results requestData(data, params){ var query = this.db.table(this.dbTable); //check if any filters have been set on the query params if(params.filter){ params.filter.forEach((filter) => { query = query.filter(filter.field, filter.type, filter.value); }) } //check if any sorters have been set on the query params if(params.sort){ params.sort.forEach((sorter) => { query = query.sort(sorter.field, sorter.dir); }) } return query.run(); }
Complete Module Code
So if we put all of that together we get the following module code:
import {Tabulator, Module} from 'tabulator-tables'; class DBModule extends Module{ constructor(table){ super(table); this.db = null; //hold the database this.dbTable = null; //hold the database table name //register table options this.registerTableOption("database", false); this.registerTableOption("databaseTable", ""); } //called by the table when it is ready for module interactions initialize(){ //check to see if module has been enabled before subscribing to events if(this.options("database") && this.options("databaseTable")){ //copy table options to local variables for ease of access this.db = this.options("database"); this.dbTable = this.options("databaseTable"); //subscribe to events this.subscribe("data-loading", this.requestDataCheck.bind(this)); this.subscribe("data-load", this.requestData.bind(this)); } } //request a remote data load if all the table options were setup correctly requestDataCheck(data, params, config, silent){ return !!(this.db && this.dbTable); } //build database query, and return results requestData(data, params){ var query = this.db.table(this.dbTable); //check if any filters have been set on the query params if(params.filter){ params.filter.forEach((filter) => { query = query.filter(filter.field, filter.type, filter.value); }) } //check if any sorters have been set on the query params if(params.sort){ params.sort.forEach((sorter) => { query = query.sort(sorter.field, sorter.dir); }) } //run query and return promise return query.run(); } } DBModule.moduleName = "db"; Tabulator.registerModule(DBModule);