Skip to content

Instantly share code, notes, and snippets.

@mattparker
Created February 23, 2010 00:00
Show Gist options
  • Save mattparker/311684 to your computer and use it in GitHub Desktop.
Save mattparker/311684 to your computer and use it in GitHub Desktop.
<!doctype html>
<html>
<head>
<title>Extending DataTable - showing and hiding columns</title>
<link type="text/css" rel="stylesheet" href="http://developer.yahoo.com/yui/3/assets/yui.css">
<link type="text/css" rel="stylesheet" href="http://yui.yahooapis.com/3.0.0/build/cssfonts/fonts-min.css">
<style type="text/css">
/* Any special style you need beyond the usual skinning */
</style>
<!-- Combo-handled YUI CSS files: -->
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/combo?2.8.0r4/build/datatable/assets/skins/sam/datatable.css&2.8.0r4/build/menu/assets/skins/sam/menu.css">
<!-- Combo-handled YUI JS files: -->
<script type="text/javascript" src="http://yui.yahooapis.com/combo?2.8.0r4/build/yahoo/yahoo.js&2.8.0r4/build/dom/dom.js&2.8.0r4/build/event/event.js&2.8.0r4/build/element/element.js&2.8.0r4/build/datasource/datasource.js&2.8.0r4/build/datatable/datatable.js&2.8.0r4/build/container/container_core.js&2.8.0r4/build/menu/menu.js"></script>
<script type="text/javascript" src="http://github.com/mattparker/Yui-DataTable-extension---column-chooser/raw/master/lplt-datatable.js"></script>
</head>
<body id="yahoo-com" class="yui-skin-sam">
<div id="doc3" class="yui-t2">
<div id="bd">
<div id="yui-main">
<div class="yui-b">
<div class="yui-ge">
<div id="main" class="yui-u first example">
<h2>DataTable: Column visibility context menu</h2>
<div id="example" class="promo">
<div class="example-intro">
<p> DataTables and their datasources can hold more data than you might want
to display - the example Showing, Hiding and Reordering Columns
<a href="http://developer.yahoo.com/yui/examples/datatable/dt_colshowhide.html"
title="Datatable column show and hide example">
http://developer.yahoo.com/yui/examples/datatable/dt_colshowhide.html</a>
shows one way to add column showing/hiding to a datatable.
</p>
<p>
Sometimes you want features like this on all your DataTables (or any widget):
then it might be time to extend the widget. Satyam's written a couple of
articles on getting started with exending the YUI library:
<a href="http://yuiblog.com/blog/2008/06/24/buildingwidgets"
title="Satyams article on builing your own library and extending from Element">
http://yuiblog.com/blog/2008/06/24/buildingwidgets</a>.
</p>
<p>
This example shows how to take an example and build it in. We're going to use
a ContextMenu that's triggered by a right-click on the table header. The
items on the ContextMenu will be the column labels, with some checks showing
which columns are currently visible. The ID column is initially hidden.
</p>
</div>
<div class="module example-container">
<div id="example-canvas" class="bd">
<p> <!-- Container for the table -->
<div id="basic"></div>
</p>
<script>
(function(){
var Data = {
bookorders: [
{id:"po-0167", date:new Date(1980, 2, 24), quantity:1, amount:4, title:"A Book About Nothing"},
{id:"po-0783", date:new Date("January 3, 1983"), quantity:null, amount:12.12345, title:"The Meaning of Life"},
{id:"po-0297", date:new Date(1978, 11, 12), quantity:12, amount:1.25, title:"This Book Was Meant to Be Read Aloud"},
{id:"po-1482", date:new Date("March 11, 1985"), quantity:6, amount:3.5, title:"Read Me Twice"}
]
};
var myColumnDefs = [
{key:"id",label:"ID", hidden:true, sortable:true, resizeable:true},
{key:"date", formatter:YAHOO.widget.DataTable.formatDate, sortable:true, sortOptions:{defaultDir:YAHOO.widget.DataTable.CLASS_DESC},resizeable:true},
{key:"quantity", formatter:YAHOO.widget.DataTable.formatNumber, sortable:true, resizeable:true},
{key:"amount", label: "Cost" ,formatter:YAHOO.widget.DataTable.formatCurrency, sortable:true, resizeable:true},
{key:"title", sortable:true, resizeable:true}
];
var myDataSource = new YAHOO.util.DataSource( Data.bookorders);
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSARRAY;
myDataSource.responseSchema = {
fields: ["id","date","quantity","amount","title"]
};
myDataTable = new YAHOO.LPLT.DataTable("basic",
myColumnDefs, myDataSource, {showColumnChooser: true,
// And just because I'm British...
dateOptions: { format: "%d/%m/%Y", locale: "en" },
currencyOptions :{ prefix: "&pound;" }});
})();
</script>
</div>
</div>
</div>
<h3 class="first">Extending the DataTable widget</h3>
<p>We start by creating the constructor for the new widget, all wrapped in an
anonymous function with some shortcuts:</p>
<textarea>
(function(){
var Lang = YAHOO.lang,
Dom = YAHOO.util.Dom;
/**
* Constructor: parent does the work, and we add a listener to set up
* the columnchooser
*/
var lpltTable = function( el , oColDef, oDS, oCfg ){
lpltTable.superclass.constructor.call( this, el , oColDef, oDS, oCfg );
// Events when we need to build/rebuild the menu
this.on( "initEvent" , this.initColumnChooser, this, true );
this.on( "columnInsertEvent" , this.refreshColumnChooserMenu, this, true );
this.on( "columnRemoveEvent" , this.refreshColumnChooserMenu, this, true );
this.on( "columnReorderEvent" , this.refreshColumnChooserMenu, this, true );
};
</textarea>
<p>
The call to the superclass constructor will get us a nice new DataTable. Once
we've done that we listen to some events that we'll be interested in. The first,
initEvent, will set up the context menu; the other three will refresh it if
we later decide to add columns, remove them, or re-order them. These events
call methods that don't exist yet, initColumnChooser and refreshColumnChooserMenu
- they'll come shortly.
</p>
<h4 id="step2">Extending - adding config options</h4>
<p>
Next we need to add the code to make the context menu and so on. We do this
by extending YAHOO.widget.DataTable to our new constructor. The third argument
is an object with the properties and methods we want to add to the base
DataTable.</p>
<textarea>YAHOO.extend( lpltTable, YAHOO.widget.DataTable, {
/**
* @description Reference to the column chooser context menu
* @type Object
*/
_columnChooserMenu:null,
initAttributes: function( oCfg ){
lpltTable.superclass.initAttributes.call( this, oCfg );
/**
* @attribute showColumnChooser
* @description Should the column chooser menu should be used
* @type Bool
* @default false
*/
this.setAttributeConfig( 'showColumnChooser' , {
validator: function(v){ return Lang.isBoolean( v ); },
value: false
}
);
},
</textarea>
<p> The first things to do are to add a property to store the ContextMenu in,
and then add an initAttributes method. This allows us to add config options
really easily. As before we call the 'parent' class initAttributes first,
and then add a simple config option, showColumnChooser. There's a fair bit
more you can do with these - read Satyam's article if you haven't already.
</p>
<h4 id="step3">Build the context menu</h4>
<p>
Now we start doing some real work: the initColumnChooser method
sets up the ContextMenu. The trigger: this.getTheadEl() will link it
to the table header. Of course, because we've extended the DataTable we
have access to all its methods, and our scope means we can use 'this'
to refer to our new DataTable object. </p>
<textarea>
/**
* Adds a context menu that takes columns and allows them to be shown
* or hidden
*
*/
initColumnChooser: function(){
// check if column chooser should be used:
if( !this.get( "showColumnChooser" ) ){ return; }
// create a context menu and add items to it, one for each column
var cMenu = new YAHOO.widget.ContextMenu( Dom.generateId( ),
{ trigger: this.getTheadEl() } );
this._addColumnChooserMenuItems( cMenu );
// render the menu
cMenu.render( this.get("element") );
// menu click handler
cMenu.subscribe( "click", this.doColumnChooser, this, true );
// And a column show/hide handler that updates the menu check states.
// Doing it this way means that if columns are shown/hidden in other
// ways the menu will be up to date
this.subscribe( "columnHideEvent" , this.updateColumnChooser, this, true );
this.subscribe( "columnShowEvent" , this.updateColumnChooser, this, true );
// finally, make sure the context menu is destroyed when the table is:
this.subscribe( "beforeDestroyEvent" , this.destroyColumnChooser , this, true);
// Keep a reference to it:
this._columnChooserMenu = cMenu;
},
</textarea>
<h4 id="step4">Adding items</h4>
<p>
So this method creates a ContextMenu, renders it, and listens for important
events. But we're going to want to add items to the menu more than
once, so we outsource them to _addColumnChooserMenuItems.
</p>
<textarea> /**
* Adds columns to the column chooser context menu
* @param {YAHOO.widget.ContextMenu} Context menu to add items to
*/
_addColumnChooserMenuItems: function( cMenu ){
// loop through the columns:
var allColumns = this.getColumnSet().keys;
for( var i = 0, l = allColumns.length; i < l; i++ ){
cMenu.addItem( { text: allColumns[ i ].label || allColumns[ i ].getKey(),
value: allColumns[ i ].getKey() ,
checked: !allColumns[i].hidden } );
}
},
</textarea>
<p>
We loop through the ColumnSet and add items to the context menu.
We'll use the key for the value of the menu item so we can easily
find our column later when someone clicks on it.
</p>
<h4 id="step5">Keeping up-to-date</h4>
<p> The next two methods are make sure things are up to date if columns get
hidden, moved etc by other means. They loop through, either re-adding
the items to the menu, or updating the checks.
</p>
<textarea> /**
* Clears the column chooser context menu and re-reads columns
*/
refreshColumnChooserMenu: function(){
var cMenu = this._columnChooserMenu;
if( cMenu === undefined || cMenu === null ){
return false;
}
cMenu.clearContent();
this._addColumnChooserMenuItems( cMenu );
this._columnChooserMenu = cMenu;
},
/**
* Updates the 'checked' state of the menu based on visibility of cols.
*/
updateColumnChooser: function( ){
// loop through the columns:
var allColumns = this.getColumnSet().keys,
cMenu = this._columnChooserMenu;
for( var i = 0, l = allColumns.length; i < l; i++ ){
cMenu.getItem( i ).cfg.setProperty( "checked", !allColumns[i].hidden );
}
},
</textarea>
<h4 id="step6">Handling a context menu click</h4>
<p>
Up to now, we'll have a context menu OK, but clicking won't do anything
(except throw an error): we need to write the handler for the context menu
click.</p>
<textarea>
/**
* Handler for column chooser context menu
* @param String will be "click"
* @param Array Arguments passed: first is Event Object, second is the
* MenuItem that was clicked
* @param Object The menu
*/
doColumnChooser: function( strEv, obEv, menu ){
// This is the menuitem
var tar = obEv[1],
// This is the column:
col = this.getColumn( tar.value );
// If column is hidden, show it:
if( col.hidden ){
this.showColumn( col );
}
else{
this.hideColumn( col );
}
},
</textarea>
<p>
The second argument gives us access to the menuitem that was clicked,
and the value of the menuitem is the key for the column, so it's
easy to grab the column and show or hide it.
</p>
<h4 id="step7">Destroying it all </h4>
<p>
Finally, we keep things tidy. If you've got a lot going on on your
app, you'll likely need to do some tidying up after yourself
to stop the DOM getting too bloated and filling up with listeners until
it all grinds to a halt. One of the keys to this is destroying widgets
when you're done with them.
</p>
<p>
By default there's no destroy event in DataTable, but there is a
destroy method. But we need to know
when the table's being destroyed so we can destroy the ContextMenu. Fortunately,
it's straightforward to add an event: </p>
<textarea>
/**
* Over-ride destroy to fire an event first and then do the destruction
*/
destroy: function(){
this.fireEvent( "beforeDestroyEvent" );
lpltTable.superclass.destroy.call( this );
} ,
</textarea>
<p>
Right back when we were setting up the ContextMenu we subscribed to the
beforeDestroyEvent (that we've just created)
to call destroyColumnChooser: so we'd better destroy
the ContextMenu:
</p>
<textarea>
destroyColumnChooser: function(){
if( this._columnChooserMenu !== undefined ){
this._columnChooserMenu.destroy();
}
}
</textarea>
<h4 id="step8">Adding it to our namespace.</h4>
<p>
Everything we've done up to now is in an anonymous function, so we'll
give ourselves a namespace to make it accessible. Of course you can
use any namespace you like.
</p>
<textarea>
YAHOO.namespace("LPLT");
YAHOO.LPLT.DataTable = lpltTable;
</textarea>
<h3 id="allcode">Full Code listing</h3>
<textarea>
(function(){
var Lang = YAHOO.lang,
Dom = YAHOO.util.Dom;
/**
* Constructor: parent does the work, and we add a listener to set up
* the columnchooser
*/
var lpltTable = function( el , oColDef, oDS, oCfg ){
lpltTable.superclass.constructor.call( this, el , oColDef, oDS, oCfg );
// Events when we need to build/rebuild the menu
this.on( "initEvent" , this.initColumnChooser, this, true );
this.on( "columnInsertEvent" , this.refreshColumnChooserMenu, this, true );
this.on( "columnRemoveEvent" , this.refreshColumnChooserMenu, this, true );
this.on( "columnReorderEvent" , this.refreshColumnChooserMenu, this, true );
};
YAHOO.extend( lpltTable, YAHOO.widget.DataTable, {
/**
* @description Reference to the column chooser context menu
* @type Object
*/
_columnChooserMenu:null,
initAttributes: function( oCfg ){
lpltTable.superclass.initAttributes.call( this, oCfg );
/**
* @attribute showColumnChooser
* @description Should the column chooser menu should be used
* @type Bool
* @default false
*/
this.setAttributeConfig( 'showColumnChooser' , {
validator: function(v){ return Lang.isBoolean( v ); },
value: false
}
);
/**
* @attribute multiCellEditNav
* @description Turns on navigation around the table with inline editing
* @type Bool
* @default false
*/
this.setAttributeConfig( 'multiCellEditNav' , {
validator: function(v){ return Lang.isBoolean( v ); },
value: false,
method: function( v ){
if ( v === true ){
this.on( "editorKeydownEvent", this.moveCellsWhileEditing , this , true );
this.on( "editorShowEvent", this.keepEditorVisible, this, true );
}
}
}
);
},
/**
* Adds a context menu that takes columns and allows them to be shown
* or hidden
*
*/
initColumnChooser: function(){
// check if column chooser should be used:
if( !this.get( "showColumnChooser" ) ){ return; }
// create a context menu and add items to it, one for each column
var cMenu = new YAHOO.widget.ContextMenu( Dom.generateId( ),
{ trigger: this.getTheadEl() } );
this._addColumnChooserMenuItems( cMenu );
// render the menu
cMenu.render( this.get("element") );
// menu click handler
cMenu.subscribe( "click", this.doColumnChooser, this, true );
// And a column show/hide handler that updates the menu check states.
// Doing it this way means that if columns are shown/hidden in other
// ways the menu will be up to date
this.subscribe( "columnHideEvent" , this.updateColumnChooser, this, true );
this.subscribe( "columnShowEvent" , this.updateColumnChooser, this, true );
// finally, make sure the context menu is destroyed when the table is:
this.subscribe( "beforeDestroyEvent" , this.destroyColumnChooser , this, true);
// Keep a reference to it:
this._columnChooserMenu = cMenu;
},
/**
* Clears the column chooser context menu and re-reads columns
*/
refreshColumnChooserMenu: function(){
var cMenu = this._columnChooserMenu;
if( cMenu === undefined || cMenu === null ){
return false;
}
cMenu.clearContent();
this._addColumnChooserMenuItems( cMenu );
this._columnChooserMenu = cMenu;
},
/**
* Updates the 'checked' state of the menu based on visibility of cols.
*/
updateColumnChooser: function( ){
// loop through the columns:
var allColumns = this.getColumnSet().keys,
cMenu = this._columnChooserMenu;
for( var i = 0, l = allColumns.length; i < l; i++ ){
cMenu.getItem( i ).cfg.setProperty( "checked", !allColumns[i].hidden );
}
},
/**
* Adds columns to the column chooser context menu
* @param {YAHOO.widget.ContextMenu} Context menu to add items to
*/
_addColumnChooserMenuItems: function( cMenu ){
// loop through the columns:
var allColumns = this.getColumnSet().keys;
for( var i = 0, l = allColumns.length; i < l; i++ ){
cMenu.addItem( { text: allColumns[ i ].label || allColumns[ i ].getKey(),
value: allColumns[ i ].getKey() ,
checked: !allColumns[i].hidden } );
}
},
/**
* Handler for column chooser context menu
* @param String will be "click"
* @param Array Arguments passed: first is Event Object, second is the
* MenuItem that was clicked
* @param Object The menu
*/
doColumnChooser: function( strEv, obEv, menu ){
// This is the menuitem
var tar = obEv[1],
// This is the column:
col = this.getColumn( tar.value );
// If column is hidden, show it:
if( col.hidden ){
this.showColumn( col );
}
else{
//tar.cfg.setProperty( "checked" , true );
this.hideColumn( col );
}
},
/**
* Destroys the column chooser context menu.
*/
destroyColumnChooser: function(){
if( this._columnChooserMenu !== undefined ){
this._columnChooserMenu.destroy();
}
},
/**
* Over-ride destroy to fire an event first and then do the destruction
*/
destroy: function(){
this.fireEvent( "beforeDestroyEvent" );
lpltTable.superclass.destroy.call( this );
}
}
);
YAHOO.namespace( "LPLT" );
YAHOO.LPLT.DataTable = lpltTable;
})();
</textarea>
<h3>About...</h3>
<p style="margin-top: 2em;">Matt Parker is the technical director
at <a href="http://www.lamplightdb.co.uk" title="Lamplight Database Systems Limited">
Lamplight Database Systems,</a> a small company based in London, UK
that supplies a comprehensive management system to charities
and non-profits.
</p>
</div>
<div class="yui-u sidebar">
<div class="mod box4" style="height: 400px; line-height: 200px; text-align: center;">(sidebar)</div>
</div>
</div>
</div>
</div>
<div class="yui-b toc3" style="height: 600px; line-height: 200px; text-align: center;">(left nav)</div>
</div>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment