Job Opportunity - JavaScript Developer
Under Development
This version is currently under development and should not be used in a production environment.

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 intializing 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 togeather 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 proccessing 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 Hanlder

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 hanlder 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 hanlder 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 togeather 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 hanlder 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 paramters 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 paramters
  • 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 togeather 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);