Monday 22nd September, 2014

Alphabet input search - Part III

In the final part of this series of posts that explores some of the search options and APIs for extending DataTables I will detail how to convert the alphabet search that was created in the previous two parts of this series into a feature plug-in for DataTables with API extension methods that can be used to control the new feature that we have created.

The end result is a customisable extension for DataTables that can easily be reused for one or more tables on any web-page using the simple initialisation:

var table = $('#example').DataTable( {
    layout: {
        top1: 'alphabetSearch'
    }
} );

Or if you are using legacy DataTables 1.x:

var table = $('#example').DataTable( {
    dom: 'Alfrtip'
} );

Note the use of dom to specify the table control elements that are used - the A is our new plug-in. That is all there is to using it!

Before we dive into the code, the end result is now available on the DataTables CDN (Javascript / CSS) and a demonstration of the resulting table is shown below (it is functionality the same as that which was created in part II).

NamePositionOfficeAgeStart dateSalary
Tiger NixonSystem ArchitectEdinburgh612011-04-25$320,800
Garrett WintersAccountantTokyo632011-07-25$170,750
Ashton CoxJunior Technical AuthorSan Francisco662009-01-12$86,000
Cedric KellySenior Javascript DeveloperEdinburgh222012-03-29$433,060
Airi SatouAccountantTokyo332008-11-28$162,700
Brielle WilliamsonIntegration SpecialistNew York612012-12-02$372,000
Herrod ChandlerSales AssistantSan Francisco592012-08-06$137,500
Rhona DavidsonIntegration SpecialistTokyo552010-10-14$327,900
Colleen HurstJavascript DeveloperSan Francisco392009-09-15$205,500
Sonya FrostSoftware EngineerEdinburgh232008-12-13$103,600
Jena GainesOffice ManagerLondon302008-12-19$90,560
Quinn FlynnSupport LeadEdinburgh222013-03-03$342,000
Charde MarshallRegional DirectorSan Francisco362008-10-16$470,600
Haley KennedySenior Marketing DesignerLondon432012-12-18$313,500
Tatyana FitzpatrickRegional DirectorLondon192010-03-17$385,750
Michael SilvaMarketing DesignerLondon662012-11-27$198,500
Paul ByrdChief Financial Officer (CFO)New York642010-06-09$725,000
Gloria LittleSystems AdministratorNew York592009-04-10$237,500
Bradley GreerSoftware EngineerLondon412012-10-13$132,000
Dai RiosPersonnel LeadEdinburgh352012-09-26$217,500
Jenette CaldwellDevelopment LeadNew York302011-09-03$345,000
Yuri BerryChief Marketing Officer (CMO)New York402009-06-25$675,000
Caesar VancePre-Sales SupportNew York212011-12-12$106,450
Doris WilderSales AssistantSydney232010-09-20$85,600
Angelica RamosChief Executive Officer (CEO)London472009-10-09$1,200,000
Gavin JoyceDeveloperEdinburgh422010-12-22$92,575
Jennifer ChangRegional DirectorSingapore282010-11-14$357,650
Brenden WagnerSoftware EngineerSan Francisco282011-06-07$206,850
Fiona GreenChief Operating Officer (COO)San Francisco482010-03-11$850,000
Shou ItouRegional MarketingTokyo202011-08-14$163,000
Michelle HouseIntegration SpecialistSydney372011-06-02$95,400
Suki BurksDeveloperLondon532009-10-22$114,500
Prescott BartlettTechnical AuthorLondon272011-05-07$145,000
Gavin CortezTeam LeaderSan Francisco222008-10-26$235,500
Martena MccrayPost-Sales supportEdinburgh462011-03-09$324,050
Unity ButlerMarketing DesignerSan Francisco472009-12-09$85,675
Howard HatfieldOffice ManagerSan Francisco512008-12-16$164,500
Hope FuentesSecretarySan Francisco412010-02-12$109,850
Vivian HarrellFinancial ControllerSan Francisco622009-02-14$452,500
Timothy MooneyOffice ManagerLondon372008-12-11$136,200
Jackson BradshawDirectorNew York652008-09-26$645,750
Olivia LiangSupport EngineerSingapore642011-02-03$234,500
Bruno NashSoftware EngineerLondon382011-05-03$163,500
Sakura YamamotoSupport EngineerTokyo372009-08-19$139,575
Thor WaltonDeveloperNew York612013-08-11$98,540
Finn CamachoSupport EngineerSan Francisco472009-07-07$87,500
Serge BaldwinData CoordinatorSingapore642012-04-09$138,575
Zenaida FrankSoftware EngineerNew York632010-01-04$125,250
Zorita SerranoSoftware EngineerSan Francisco562012-06-01$115,000
Jennifer AcostaJunior Javascript DeveloperEdinburgh432013-02-01$75,650
Cara StevensSales AssistantNew York462011-12-06$145,600
Hermione ButlerRegional DirectorLondon472011-03-21$356,250
Lael GreerSystems AdministratorLondon212009-02-27$103,500
Jonas AlexanderDeveloperSan Francisco302010-07-14$86,500
Shad DeckerRegional DirectorEdinburgh512008-11-13$183,000
Michael BruceJavascript DeveloperSingapore292011-06-27$183,000
Donna SniderCustomer SupportNew York272011-01-25$112,000
NamePositionOfficeAgeStart dateSalary

It is worth noting that I will assume that you are familiar with the code developed in part I and part II of this series. As the majority of the code required for this feature plug-in has already been developed in those posts I will be shortening those code blocks. This post is simply about rearranging the code into a reusable plug-in for DataTables.

Feature plug-ins

The feature plug-ins section of the manual describes in detail how to create a feature plug-in for DataTables. The basics are that we need to register a new feature plug-in that can be used with dom through the $.fn.dataTable.ext.feature array. We provide the letter to be registered and a callback function that is executed when the plug-in is to be used for a DataTable - this callback must return the node that is to be inserted into the document for the control, in this case the alphabet search bar.

$.fn.dataTable.ext.feature.push( {
    fnInit: function ( settings ) {
        var search = new $.fn.dataTable.AlphabetSearch( settings );
        return search.node();
    },
    cFeature: 'A'
} );

In the above code we register the character A and create a new instance of $.fn.dataTable.AlphabetSearch, returning the node from its own API node() method.

$.fn.dataTable.AlphabetSearch is new in our code, but it is simply an encapsulation of the code developed previously to create the alphabet search input nodes:

$.fn.dataTable.AlphabetSearch = function ( context ) {
    var table = new $.fn.dataTable.Api( context );
    var alphabet = $('<div class="alphabet"/>');

    // Bin the data and create the alphabet search input element
    ...

    // API method to get the alphabet container node
    this.node = function () {
        return alphabet;
    };
};

Note the addition of the node() API method that can be used to get the container node, as is done in the registration of the DataTables feature above.

Alternative initialisation

It is worth pointing out that the alphabet search can also be initialised directly use new $.fn.dataTable.AlphabetSearch() rather than using the dom option. This can improve flexibility and control over where you insert the alphabet bar if you so wish:

var table = $('#myTable').DataTable();
var search = new $.fn.dataTable.AlphabetSearch();

$( search.node() ).appendTo( ... ); // insert into document

Search plug-in update

Previously the search plug-in used the variable _alphabetSearch to determine what letter to search for, however that was not isolated to a single table - it would apply to all tables on a page. That is not suitable for a reusable component so we need to use a different method to store what letter to search for.

For this we can use the DataTables settings object which is globally unique for each table on the page (the settings object is referred to as the "context" in much of the API documentation). By simply attaching our parameter to the settings object we can always access it for that table. Consider the following update to the search plug-in that we developed way back in part I:

$.fn.dataTable.ext.search.push( function ( context, searchData ) {
    // Ensure that there is a search applied to this table before running it
    if ( ! context.alphabetSearch ) {
        return true;
    }

    if ( searchData[0].charAt(0) === context.alphabetSearch ) {
        return true;
    }

    return false;
} );

The change is simply to use context.alphabetSearch rather than _alphabetSearch. Now to trigger a search we just need to set that parameter, which is where the API plug-ins come in.

API plug-ins

An API plug-in can extend the default set of API methods that DataTables presents, and in this case we wish to add the ability to set the alphabetSearch parameter for each table. We can do this very easily using the following:

$.fn.dataTable.Api.register( 'alphabetSearch()', function ( searchTerm ) {
    this.iterator( 'table', function ( context ) {
        context.alphabetSearch = searchTerm;
    } );

    return this;
} );

Above the iterator() method is used to loop over each table in the API instance's context and set the search term. Although our feature plug-in doesn't make use of the multi-table aspect of the DataTables API, it would be quite possible to use $('table.dataTable').DataTable().alphabetSearch( 'A' ); to search for A in all tables on a page due to the use of iterator() above.

For completeness lets also create a method that can be used to re-bin the data - updating the counts that are available for each letter as shown on mouse over:

$.fn.dataTable.Api.register( 'alphabetSearch.recalc()', function ( searchTerm ) {
    this.iterator( 'table', function ( context ) {
        draw(
            new $.fn.dataTable.Api( context ),
            $('div.alphabet', this.table().container()) );
        );
    } );

    return this;
} );

This uses the draw() method that is simply a function that encapsulates the draw code developed previously. It is passed in an API instance for the current table and the alphabet search DOM node for the table in question.

Final code

We've reached the end of this foray into the world of DataTables extensions and I hope you've found this to be a useful introduction into how to create reusable plug-ins that customise DataTables to your needs.

As always, feedback is very welcome on this and any other aspect of DataTables!

The final code developed in this series of posts is shown below and is available on the DataTables CDN (Javascript / CSS).

(function(){


// Search function
$.fn.dataTable.Api.register( 'alphabetSearch()', function ( searchTerm ) {
    this.iterator( 'table', function ( context ) {
        context.alphabetSearch = searchTerm;
    } );

    return this;
} );

// Recalculate the alphabet display for updated data
$.fn.dataTable.Api.register( 'alphabetSearch.recalc()', function ( searchTerm ) {
    this.iterator( 'table', function ( context ) {
        draw(
            new $.fn.dataTable.Api( context ),
            $('div.alphabet', this.table().container())
        );
    } );

    return this;
} );


// Search plug-in
$.fn.dataTable.ext.search.push( function ( context, searchData ) {
    // Ensure that there is a search applied to this table before running it
    if ( ! context.alphabetSearch ) {
        return true;
    }

    if ( searchData[0].charAt(0) === context.alphabetSearch ) {
        return true;
    }

    return false;
} );


// Private support methods
function bin ( data ) {
    var letter, bins = {};

    for ( var i=0, ien=data.length ; i<ien ; i++ ) {
        letter = data[i].charAt(0).toUpperCase();

        if ( bins[letter] ) {
            bins[letter]++;
        }
        else {
            bins[letter] = 1;
        }
    }

    return bins;
}

function draw ( table, alphabet )
{
    alphabet.empty();
    alphabet.append( 'Search: ' );

    var columnData = table.column(0).data();
    var bins = bin( columnData );

    $('<span class="clear active"/>')
        .data( 'letter', '' )
        .data( 'match-count', columnData.length )
        .html( 'None' )
        .appendTo( alphabet );

    for ( var i=0 ; i<26 ; i++ ) {
        var letter = String.fromCharCode( 65 + i );

        $('<span/>')
            .data( 'letter', letter )
            .data( 'match-count', bins[letter] || 0 )
            .addClass( ! bins[letter] ? 'empty' : '' )
            .html( letter )
            .appendTo( alphabet );
    }

    $('<div class="alphabetInfo"></div>')
        .appendTo( alphabet );
}


$.fn.dataTable.AlphabetSearch = function ( context ) {
    var table = new $.fn.dataTable.Api( context );
    var alphabet = $('<div class="alphabet"/>');

    draw( table, alphabet );

    // Trigger a search
    alphabet.on( 'click', 'span', function () {
        alphabet.find( '.active' ).removeClass( 'active' );
        $(this).addClass( 'active' );

        table
            .alphabetSearch( $(this).data('letter') )
            .draw();
    } );

    // Mouse events to show helper information
    alphabet
        .on( 'mouseenter', 'span', function () {
            alphabet
                .find('div.alphabetInfo')
                .css( {
                    opacity: 1,
                    left: $(this).position().left,
                    width: $(this).width()
                } )
                .html( $(this).data('match-count') );
        } )
        .on( 'mouseleave', 'span', function () {
            alphabet
                .find('div.alphabetInfo')
                .css('opacity', 0);
        } );

    // API method to get the alphabet container node
    this.node = function () {
        return alphabet;
    };
};

$.fn.DataTable.AlphabetSearch = $.fn.dataTable.AlphabetSearch;


// Register a search plug-in
$.fn.dataTable.ext.feature.push( {
    fnInit: function ( settings ) {
        var search = new $.fn.dataTable.AlphabetSearch( settings );
        return search.node();
    },
    cFeature: 'A'
} );


}());