YUI Tutorial: Subclassing DataTable to Create DataView

By YUI TeamApril 23rd, 2007

Victor Morales is a software engineer at DePaul University. Originally from Mexico, Victor’s passion for technology began after learning programming by himself on an Apple IIe computer at age 10. He recently became interested in JavaScript as a development platform, and he’s been one of the most active contributors lately to the YUI developer forums.

In this YUI Blog tutorial, Victor explores the process of integrating the new YUI DataTable with the ContextMenu class of the YUI Menu Control as well as with the AutoComplete Control.

In this tutorial we will build a subclass for DataTable called DataView. This subclass will allow the DataTable to hide a particular column by right clicking in the table header rows and selecting a column from a ContextMenu — one of the major types of menus supported by the YUI Menu Control.

The first step to subclassing DataTable is writing the constructor for the new subclass (DataView, in this case) and specifying its inheritance from the DataTable class using YAHOO.extend:

 //create namespace:
 YAHOO.namespace("yuiblog.widget");

  YAHOO.yuiblog.widget.DataView = function(elContainer , oColumnSet , oDataSource , oConfigs) {
         if (arguments.length > 0) {
                YAHOO.yuiblog.widget.DataView.superclass.constructor.call(this, elContainer , oColumnSet , oDataSource , oConfigs);
           }
           //Call ContextMenu initialization method
           this._initHideMenu();
 };
 // Inherit from YAHOO.widget.DataTable
 YAHOO.lang.extend(YAHOO.yuiblog.widget.DataView, YAHOO.widget.DataTable);
 

The DataView constructor adds a hookup to the _initHideMenu method, which initializes the ContextMenu. This method has the following responsibilites:

  1. Initialize and create a ContextMenu instance.
  2. Determine if a column can be hidden, and if so add it as an item in the ContextMenu.
  3. Subscribe each MenuItem to the onhideMenuClick event handler.
 YAHOO.yuiblog.widget.DataView.prototype._initHideMenu=function(oColumnSet) {

         var oColumnSet= this._oColumnSet
         this.aColState=[];
         var _hideCol=[]
         var keys= oColumnSet.keys;
         for (var i=0; i<keys.length;i++) {
             if(keys[i].hideable) {
                     itemText = keys[i].text || keys[i].key;
                 _hideCol.push({text:itemText,checked:true, colNum:i})
              }
             this.aColState[i]=0; 
         }
         if (_hideCol.length>0)    {
           var oContextMenu = new YAHOO.widget.ContextMenu("hideMenu", { trigger: this.getHead()     } );

           // Define the items for the menu
           var aMenuItemData =_hideCol 
           var nMenuItems = aMenuItemData.length;
           var oMenuItem;
           for(var i=0; i<nMenuItems; i++) {
              var item= aMenuItemData[i]
              oMenuItem = oContextMenu.addItem(item);
              oMenuItem.clickEvent.subscribe(this.onhideMenuClick, [oMenuItem,item.colNum],this);
           }
           oContextMenu.render(document.body);
         }

 }; 

Notice how _initHideMenu iterates over the ColumnSet keys array, which maps one-to-one to a table column. If a column has the hideable property set to true, an anonymous object is created with the column text and position and then “pushed” into an array that is used to populate the ContextMenu.

The next step is to define the onHideMenuClick method, which hides the appropriate column from the DataTable depending on the MenuItem that was clicked. To hide a column we simply call the hideSwap method which alters the display attribute of the column.

 YAHOO.yuiblog.widget.DataView.prototype.onhideMenuClick=function(p_sType, p_aArgs, p_oMenuItem) {
         var oMenuItem= p_oMenuItem[0];
         var col_no=p_oMenuItem[1];
         var swap= oMenuItem.cfg.getProperty("checked")
         oMenuItem.cfg.setProperty("checked", swap);
         var colstyle;
         if (!swap) {
             this.hideSwap(col_no,'none',0)
             this.aColState[col_no]=1      
         }
         else {
             this.hideSwap(col_no,'',0)
             this.aColState[col_no]=0
         }
 };
 YAHOO.widget.DataView.prototype.hideSwap=function(col_no,colstyle,startRow) {
       //Hide or unhide column header
        var headRow= this.getHead().getElementsByTagName('th')
        headRow[col_no].style.display=colstyle;

        var rows= this.getBody().getElementsByTagName('tr')

        // Hide or unhide column rows 
        for (var row=startRow; row<rows.length;row++) {
          var cels = rows[row].getElementsByTagName('td')
          cels[col_no].style.display=colstyle;
        }
};
 

With these changes in place, in our HTML page we only need to make sure to specify which columns will be “hideable”.

        var myColumnHeaders = [
             {key:"POID", abbr:"Purchase order ID", sortable:true, resizeable:true },
             {key:"Date", type:"date", sortable:true, resizeable:true, hideable:true},
             {key:"Quantity", type:"number", sortable:true, resizeable:true,hideable:true},
             {key:"Amount", type:"currency", sortable:true, resizeable:true,hideable:true},
             {key:"Title", text:"Book Title", type:"string", sortable:true, resizeable:true,hideable:true}
         ]; 

Click here for a functional example of the the project at this stage.

Adding Filtering to the DataView

Sometimes it is useful to view only a particular type of information and hide the rest. Filtering data from a table is a simple yet powerful pattern that allows users to find the information that they want in less time. Here we will leverage the AutoComplete Control and combine it with DataTable.

We’ll continue building upon the DataView class described above. Let’s take a quick look at the methods and properties needed to implement row filtering in our DataView class:

Name Responsibility Type
defaultView Store the original (unfiltered) table records Array property
isFiltered Keeps track of the state of the table boolean property
doBeforeLoadData Populates the defaultView array method (overriden)
filterRows Updates the content of the table method


The corresponding code is shown below:

 YAHOO.yuiblog.widget.DataView.prototype.isFiltered=false;

  YAHOO.yuiblog.widget.DataView.prototype.doBeforeLoadData= function( sRequest ,oResponse ) {
     if(oResponse) {
         this.defaultView=oResponse;
     }    
     return true;
 }

  YAHOO.yuiblog.widget.DataView.prototype.filterRows=function(filteredRows) {
     if(filteredRows == undefined) {
         this._oRecordSet.replace(this.defaultView);
         this.populateTable();
         this.isFiltered=false;
     }
     else {
         var dataView=[];
         for (var i=0; i<filteredRows.length;i++) {
              var r=filteredRows[i];
              var row= this._oRecordSet._records[r];
              dataView.push(row);
           }
           this.replaceRows(dataView);
           this._oRecordSet._records=dataView;
           this.isFiltered=true;
     }
 }; 

To initialize the defaultView property we take advantage of the doBeforeLoadData method which is automatically called by the DataView constructor once data is available.

Slightly more interesting is the filterRows method. This method receives as a parameter an array containing the row numbers that will be displayed. If we don’t specify an array then the DataView is reset to its default state and the isFiltered property is set to false.

That is really all we have to do for the DataView class. The next step is to create a subclass of AutoComplete, RowFilter, which will be responsible for "feeding" the filterRows method we just created:

 YAHOO.yuiblog.widget.RowFilter = function( elInput,elContainer,oDataTable,fnFilter,oConfigs) {
         if (arguments.length > 0) {
                YAHOO.yuiblog.widget.RowFilter.superclass.constructor.call(this, elInput,elContainer,fnFilter,oConfigs);
           }

         this.Filter=fnFilter;
         this._oDataTable=oDataTable;
          this.itemSelectEvent.subscribe(this.myOnSelect);
         this.dataReturnEvent.subscribe(this.myOnDataReturn);
         this._oDataTable.subscribe("columnSortEvent",this.updateFilter,this._oDataTable,this)
 }

                 // Inherit from YAHOO.widget.RowFilter
 YAHOO.lang.extend(YAHOO.yuiblog.widget.RowFilter, YAHOO.widget.AutoComplete); 
     

The core of the RowFilter class are the methods myOnSelect, myOnDataReturn and updateFilter. Again, a table summarizing their roles would be helpful:

Name Responsibility
myOnSelect Calls the filterRows method of its DataView instance when the user selects a result
myOnDataReturn Check if its DataView instance is filtered. If true, then it resets its DataSource and DataView instances to their original state
UpdateFilter Updates its DataSource to match the sorted DataTable

Here’s the code for each of these pieces:

myOnSelect:

 YAHOO.yuiblog.widget.RowFilter.prototype.myOnSelect= function(sType, aArgs) {
      var objResult = aArgs[2][1];
     this._oDataTable.filterRows(objResult.matchedRows)
 }
 

myOnDataReturn:

 YAHOO.yuiblog.widget.RowFilter.prototype.myOnDataReturn= function(sType, aArgs) {
      var oAutoComp = aArgs[0];
      var sQuery = aArgs[1];
      var aResults = aArgs[2];

     if(aResults.length == 0) {
           oAutoComp.setBody("<div id=\"container_default\">No matching results</div>");
      }

     this.reset();
 } 

UpdateFilter:

 YAHOO.yuiblog.widget.RowFilter.prototype.updateFilter=function(oColumn,oDataTable) {
      var records=oDataTable.getRecordSet().getRecords();
      this.Filter._aData=records;
      if (oDataTable.isFiltered) {
          this.hideColumns();
      }
 }; 

When I said that the RowFilter class is responsible for feeding the filterRows method I lied. In reality, the heavy lifting is delegated to the fnFilter method of the StringFilter class.

StringFilter Constructor:

 YAHOO.yuiblog.util.StringFilter=function(aRecords, sFieldName, oConfigs) {
       if(typeof oConfigs == "object") {
         for(var sConfig in oConfigs) {
             this[sConfig] = oConfigs[sConfig];
         }
     }
      this._aData=aRecords;
      this.schemaItem=sFieldName;
      this._init();
 };

  YAHOO.yuiblog.util.StringFilter.prototype = new YAHOO.widget.DataSource(); 

fnFilter method:

 YAHOO.yuiblog.util.StringFilter.prototype.fnFilter=function(sQuery) {
      sQuery=unescape(sQuery);
      var aResults = [];
      var aData= this._aData;
      var fName= this.schemaItem;
      if(sQuery && sQuery.length > 0) {
           var q= sQuery.toLowerCase();
           var updateResult=false;
           var elHashTable={}

           for (var i=0; i<aData.length; i++) {
                var field=aData[i][fName];
                var updateResult=false;

                 if(elHashTable[field]) {
                     //Update Hashtable entry with the additional row matched 
                      elHashTable[field].rows.push(i)
                      updateResult=true;
                }
                else {
                      elHashTable[field]= {rows:[i], resultIndex:-1};
                }

                //Save the index of the match
                var mIndex=field.toLowerCase().indexOf(q);
                var objResult={value:field, matchIndex:mIndex, matchedRows:[i]
           }

                                 if (mIndex<0) { continue;}

                                  if(updateResult){
                     var ri = elHashTable[field].resultIndex;
                     objResult.matchedRows=elHashTable[field].rows;
                     aResults[ri]=[objResult.value,objResult];
                }
                else {
                       aResults.push([objResult.value,objResult]);  
                       //Update the hashtable resultIndex   
                       elHashTable[field].resultIndex = aResults.length-1;
                       var ri= elHashTable[field].resultIndex;
                }
           }
      }
      return aResults;
 } 

The StringFilter class implements the DataSource interface and has two important properties: _aData which is a reference to the unfiltered records, and schemaItem which maps to a field name in the DataTable. When the fnFilter method receives a query from the RowFilter class it looks for the query term in the schemaItem column of its _aData array. This method returns an array containing the content of the column rows that match the query, as well as their corresponding row numbers.

Click here to try out the full DataView example including the ContextMenu and AutoComplete integration running on YUI version 2.3.1. (An older version running on version 2.2.2 is available here.)

Note:For the sake of simplicity, this particular hideColumns implementation does not work with nested headers.

[Update] Fixed display bug when user hides a column after applying a filter.

29 Comments

  1. What I’d really like to see is a DataTable that can handle server side pagination. There’s currently no way to use DataTable when your DataSet is 1000+ rows.

  2. Andrew Kelly said:
    April 23, 2007 at 6:11 pm

    Excellent tutorial, however, there is a slight bug in the example.

    Firstly filter the data and also remove some of the columns. Then click on one of the headings to sort the data, the filtered rows and columns will reappear and with a few cells missing.

    Andy.

  3. Victor Morales said:
    April 24, 2007 at 10:00 am

    @Andy:

    Thanks for your comment. I have posted an updated version correcting the bug you described.

  4. Very cool!

    I love the idea of having these kinds of YUI posts about users who are extending and mixing YUI components and utilities.

  5. Victor-

    Thank you so much!
    I see that you only posted this a couple days ago. Timing is everything I tell you. If I had started my project last week like I was supposed to I wouldn’t have seen your post :-)

  6. Andy,

    Would it be more reasonable to do filtering by just hiding rows in existing table instead of creating a new data set and rendering a new table?

  7. Victor Morales said:
    May 3, 2007 at 6:08 am

    @Artem:

    I did consider implementing filtering by just hiding rows, but then you would not have the ability to sort the table while it is in “filtered state”.

  8. Thomas Tallyce said:
    May 8, 2007 at 6:12 pm

    Am I missing something here, or would it not be much more accessible and far less obtrusive to use a standard HTML table, and just add a class to bind the JS behaviour?

  9. Excellent Tutorial !

    I’m now integrating this tutorial with editable fields and single row highlighting.

    The next thing I need to figure out is how to do wildcard searches.

    Thank you soooo much for this tutorial.
    Se lo aprecio mucho.

  10. [...] components. Jamie Curnow built an unobstrusive validator over the YUI Dom component. Victor Morales extended DataTable to add autocomplete row filtering and row hiding. Matt J. Cormier created a YUI Picker, also based on the DataTable. Caridy Patiño has been [...]

  11. Andrew King said:
    July 23, 2007 at 11:43 am

    There’s a missing ‘!’ in the 3rd line of the onHideMenuClickFunction

    var swap= oMenuItem.cfg.getProperty(“checked”)

    should be

    var swap=! oMenuItem.cfg.getProperty(“checked”)

    It’s correct on the example.

  12. Has anyone gotten this to work in YUI 2.30 ?
    Thanks.

  13. anyone on 2.3 for this subclass??

    Regards,
    Shoeb

  14. Victor Morales said:
    August 13, 2007 at 7:54 pm

    I haven’t had time to make this subclass 2.3 compatible, but I can specifically tell you that the filterRows method requires the most changes, since Record data values are no longer accessible with oRecord[key] or oRecord.key.

    I’ll try to have something working next week.

  15. YUI 2.30
    FireFox 2.0.0.5
    —-
    ERROR
    —-
    this.fireEvent is not a function

    this._elMsgTbody.style.display="";this.fireEvent("tableMsgShowEvent",{html:sHTML...

  16. Any update on getting this great subclass to work with 2.3.0?

  17. I’m also looking for 2.3.0 solution – any luck? I’m having trouble with pagination…2.3.0 seems to have much more built in for this…

  18. Victor Morales said:
    September 18, 2007 at 5:47 pm

    The source code for 2.3.0 version can be found here:
    http://www.geocities.com/andresm1981/DataView230/Dataview2.3.0.zip

    Regards,

    Victor

  19. unusable with 1.000+ rows.

  20. menuitem_checked_*.png files in the 2.4.0/build/menu/assets directory do not match the menu.css references to menuitem_checkbox_*.png files.

    As a result the check graphic in the context menu do not show. When the files and references are made consistent the menu looks as it should.

  21. Has anyone gotten this to work with 2.4.1

    I am seeing:

    YAHOO.lang.extend failed, please check that all dependencies are included.

    And applying a filter in the second example does not work but provides no error. It appears that in the Filter function, this line:

    field=this.getRecord(i).getData()[sColumnKey]

    returns undefined.

  22. Hi!

    How can I internacionalize the columns’s label?

    I´m using Struts2 and have a messages file, but I can´t access it from javascript.

    Thanks.

  23. Victor Morales said:
    February 7, 2008 at 12:35 pm

    @Pablo

    The next YUI version (2.5.0) will support hiding columns, so this subclass will be mostly obsolete.

    Regarding your question about internationalization, you would need to specify the columns server-side. You can take a look at this example by Satyam:
    http://satyam.com.ar/yui/#ServerDriven

  24. Yesterday, I tried upgrading yui to 2.5.0 and discovered that my beloved DataView would no longer work. I understand the column hiding functions are now built in, but I don’t use those. All I care about is the filtering.

    And, it’s filtering that no longer works in 2.5. At least on my application, which uses a progressively enhanced table for data.

    I’ve been working with the code, and found that the filtering does find the data. And, it does push it into the data structure. But, it never replaces the original table with the filtered one.

    For the life of me, I can’t figure out how to filter progressively enhanced tables without this class. Can somebody please help me to figure out how to get this working with the latest YUI?

    Amy

  25. About using it in 2.5.0 I discovered that the initializeTable function now takes no data object as parameter and there’s no adding the data to recordset as it was in previous versions.

    I changed the function as following to use filtering in my 2.5.0 datatable:


    initializeTable : function(oData) {
    // Reset init flag
    this._bInit = true;

    // Clear the RecordSet
    this._oRecordSet.reset();

    // adds data to recordset (missing in 2.5.0)
    var records = this._oRecordSet.addRecords(oData);

    // Clear selections
    this._unselectAllTrEls();
    this._unselectAllTdEls();
    this._aSelections = null;
    this._oAnchorRecord = null;
    this._oAnchorCell = null;

    // refresh the view (missing in 2.5.0)
    this.render();

    },

    May be someone has a solution for using filtering without modifying original YUI class?

  26. I replaced the initializeTable function with the following:

    this.getRecordSet().replaceRecords(dataView)
    this.render();

    this works for filtering but if I try sorting the filtered rows, the rows disappear and it says no records found…

  27. I am looking for YUI 2.6.0 compatible version. Client side row filtering is awesome functionality.
    I dont know YUI 2.6.0 implemented that..

    -Ram

  28. I am facing the issue described by Andy on 23rd April 2007.After hiding a column,if I sort the datatable, the hidden column reappears with header cell missing.Victor says he has fixed the issue,so where can I find the fixed version.I am currently using 2.7 of YUI but my requirement is similar in which context menu should appear after clicking on header and user should be able to show hide column.So I need this desperately.

    Thanks
    Ashish

  29. Show/hide columns does not work with draggableColumns config property of DataTable.

    Once the columns are dragged away to other position the context menu does not appear.

    Please add draggable columns feature too.