The ExtJS framework has a GridPanel object with the ability to group items into sections defined by the developer. It uses a GroupingStore, and a GroupingView in order to visually group items into a loose tree-type structure.
The biggest benefit to that mechanism is that the framework handles all the sorting, DOM elements and interaction code to make it possible.
However, if you’re not using a GridPanel (it’s a heavy component) and just want to display your data in a series of panels, there is no automatic way to group your data, so you need to roll your own code.
Luckily, the framework provides plenty of helper methods to make this a fairly trivial task. All I needed to do was extend the Ext.Panel class and, on initialization, use the collect() method on the assigned Store to determine the groups, and then use the query() method to get all records assigned to each of the groups.
See this code in action using HBox and VBox layouts.
Ext.ux.GroupedPanel = Ext.extend(Ext.Panel, {
itemConfig: {
tpl: new Ext.XTemplate(''),
selector: '',
margins: ''
},
dataConfig: {
store: new Ext.data.Store({}),
records: null,
groupColumn: ''
},
initComponent : function(){
var group_records,i=0,group_panels=[], groups;
// Initialize superclass
Ext.ux.GroupedPanel.superclass.initComponent.call(this);
// If a data structure is specified, load it into the store
if (this.dataConfig.records != null) this.dataConfig.store.load(this.dataConfig.records);
// Use the collect() method to get the unique values in the specified group column
groups = this.dataConfig.store.collect(this.dataConfig.groupColumn);
// Loop through each group
for (; i < groups.length; i=i+1){
// Use the query() method to get the records in the current group
group_records = this.dataConfig.store.query(this.dataConfig.groupColumn, groups[i]);
// Create a panel to contain the current group's records
// Use the specified XTemplate and itemSelector
group_panels[group_panels.length] = new Ext.Panel({
frame: true,
flex: 1,
margins: this.itemConfig.margins,
title: groups[i],
items: new Ext.DataView({
store: group_records,
tpl: this.itemConfig.tpl,
autoHeight: true,
itemSelector: this.itemConfig.selector
})
});
}
// Add all of the group panels to this
this.add(group_panels);
}
});
// This panel will contain all group panels, using the vbox layout to
// stack them vertically, and also stretch each panel to its width.
var ContainerPanel = new Ext.ux.GroupedPanel({
layout: 'vbox',
layoutConfig: {
align: 'stretch',
pack: 'start'
},
height:600,
dataConfig: {
store: TeammateStore,
groupColumn: 'Department',
},
itemConfig: {
tpl: new Ext.XTemplate(
'<tpl for=".">',
'<div class="teammate" id="{tmID}">',
'<div class="thumb">{tmID} - {Teammate}</div>',
'</div>',
'</tpl>'
{
compiled: true
}
),
selector: 'div.teammate',
margins: '10 0 0 0'
}
});
ContainerPanel.render('group-grid');
I can’t believe it took this long for this to click in my head. I’m sure I had seen the bubbleEvents() config option on ExtJS component documentation before, but my eyes just swept over it like when you’re looking for your kid in a crowd. the unimportant information immediately gets dismissed by your brain.
Well, today, I noticed an old bit of code in a Ext.Window object. Here’s an example:
MappingDialog = function(){
var dialog;
return {
init : function(){
dialog = new Ext.Window({
width: 200,
height: 150,
modal: true,
closable: false,
buttons: [
{ text:'Save', handler:function(){ dialog.fireEvent('saveMapping'); } },
{ text:'Cancel', handler:function(){ dialog.hide() } }
]
});
dialog.addListener('saveMapping', this.saveData, this);
},
saveData : function(){
... Save the data ...
},
showWindow : function(){ dialog.show(); },
hideWindow : function(){ dialog.hide(); }
}
};
Notice how I’m referencing the dialog object inside the Save and Cancel button handlers. That’s ugly, but at the time I wrote it I didn’t know how to do it differently. Today, however, I focused in on that dialog.fireEvent() statement and thought to myself, “Self, there has to be a way to bubble the event of a button up to its parent window, so I can avoid having direct references to the Window inside the Button – which is inside the Window.”
Off to the ExtJS API Docs and I immediately notice the bubbleEvents config option which, like I said, I’d glazed over hundreds of times in the past. remember? Well, using that, I now bubble any event up to its parent container which can handle it appropriately. Much more efficient code from a memory perspective and from a maintenance perspective as well, as renaming the Window won’t require a global search/replace.
MappingDialog = function(){
return {
init : function(){
dialog = new Ext.Window({
width: 200,
height: 150,
modal: true,
closable: false,
<strong>bubbleEvents: ['saveMapping'],</strong>
buttons: [{
text:'Save',
<strong>bubbleEvents: ['saveMapping'],</strong>
handler:function(){ this.fireEvent('saveMapping'); }
},{
text:'Cancel',
<strong>bubbleEvents: ['closeWindow'],</strong>
handler:function(){ this.fireEvent('closeWindow'); }
],
listeners: {
saveMapping : function() { this.fireEvent('saveMapping'); },
closeWindow : function() { this.close(); }
}
});
this.addListener('saveMapping', this.saveData, this);
},
saveData : function(){
... Save the data ...
},
showWindow : function(){ dialog.show(); },
hideWindow : function(){ dialog.hide(); }
}
};
Pretty straightforward example of expanding and collapsing all grouped rows by clicking on the Checkbox item.
See this code in action
// Sample JSON data for teammates, their department
// and compensation rate
var Teammates = [{
tmID: 1, Teammate:'Steve', Department:'Safety', Compensation_Rate:'30', Date_Change:'01/01/1999'},{
tmID: 2, Teammate:'Steve', Department:'IT', Compensation_Rate:'55', Date_Change:'05/07/2002'},{
tmID: 3, Teammate:'John', Department:'Sales', Compensation_Rate:'50', Date_Change:'03/05/2000'},{
tmID: 4, Teammate:'John', Department:'Executive', Compensation_Rate:'53', Date_Change:'05/10/2000'},{
tmID: 5, Teammate:'Matthew', Department:'Welding', Compensation_Rate:'55', Date_Change:'12/17/2002'
}];
// Create a Checkbox that will toggle between
// expanding and collapsing the grid items
ExpandCollapseAll = new Ext.form.Checkbox({
renderTo: Ext.getBody(),
boxLabel: 'Expand Results',
listeners: {
check: function(ctrl, val) {
if (val) {
RateGrid.view.expandAllGroups();
} else {
RateGrid.view.collapseAllGroups();
}
}
}
});
// A GridPanel using a GroupingView to display the data
// grouped by the teammate name
RateGrid = new Ext.grid.GridPanel({
renderTo: Ext.getBody(),
width: 400,
style: 'margin: 10px 10px 10px 10px',
height: 300,
store: new Ext.data.GroupingStore({
data: Teammates,
reader: new Ext.data.JsonReader({ id: 'tmID' },
Ext.data.Record.create([
{name: 'tmID', type: 'string'},
{name: 'Teammate', type: 'string'},
{name: 'Department', type: 'string'},
{name: 'Compensation_Rate', type: 'string'},
{name: 'Date_Change', type: 'string'}
])),
groupField: 'Teammate',
sortInfo: {field:'Date_Change', direction: 'ASC'}
}),
cm: new Ext.grid.ColumnModel([
{
id: 'Teammate',
dataIndex: 'Teammate',
hidden: true
},{
id: 'Department',
header: 'Department',
dataIndex: 'Department',
width: 150
},{
id: 'Compensation',
header: 'Rate',
dataIndex: 'Compensation_Rate',
width: 100
},{
id: 'DateChange',
header: 'Date',
dataIndex: 'Date_Change',
width: 100
}
]),
autoScroll: true,
autoExpandColumn: 'Department',
view: new Ext.grid.GroupingView({
showGroupName: false,
hideGroupedColumn: true,
startCollapsed: true,
groupTextTpl: '{text}'
})
});
I wanted to finally get rid of any usage of the JavaScript eval() function when parsing JSON returned from my components.
I went to JSON.org and noticed that there’s an updated json2.js file that should replace my old json.js file as well as two separate parsing engine files on the GitHub site:
Once again, I customized. I removed the existing JSON.parse() method from json2.js and replaced it with the function on json_parse_state.js, so all I need to do is
<script type="text/javascript" src="js/JSON.js"></script>
and the JSON.parse() method will now use the state engine code instead of eval().
Download custom JSON.js.
I’ve been using the AjaxCFC library for years. It’s my preferred way of integrating Javascript and ColdFusion via AJAX. I’ve even modified it from its original form so that my implementation was strictly for integration with jQuery, only returns JSON strings (ignoring WDDX and simple string), and can work with ColdSpring.
Now that I’m a heavy user of the Sencha ExtJS framework, I thought it would be useful to port the jQuery.ajaxCFC.js file over to an Ext.AjaxCFC.js file that extended the native Ext.data.Connection class and utilized the Ext.Ajax object.
Took me about half the day, but I finally got a working Ext.AjaxCFC.request() method that uses the same syntax as the $.AjaxCFC() method. For those familiar with the inner workings and code of the jQuery AjaxCFC class, this will look very familiar.
So now I can make AJAX calls using native ExtJS classes, access ColdSpring beans in my application’s bean factory, or connect directly to any CFC
Ext.AjaxCFCConnection = Ext.extend(Ext.data.Connection, {
data : null,
queryFormat : 'array',
factory : (typeof(__ajaxConfig) == 'undefined') ? null : __ajaxConfig.beanFactory,
timeout : (typeof(__ajaxConfig) == 'undefined') ? 30000 : __ajaxConfig.defaultTimeout,
url : __ajaxConfig.url,
bean : null,
request : function(arguments) {
var params = (typeof(arguments.data) == 'undefined') ? {} : arguments.data;
arguments.params = {};
arguments.params['C0-ID'] = (Math.floor(Math.random() * 10001) + "_" + new Date().getTime()).toString(),
arguments.params['method'] = 'init';
arguments.params['component'] = arguments.component;
arguments.params['bean'] = (typeof(arguments.bean) == 'undefined') ? this.bean : arguments.bean;
arguments.params['factory'] = this.factory;
arguments.params['C0-METHODNAME'] = arguments.method;
arguments.params['queryFormat'] = (typeof(arguments.queryFormat) == 'undefined') ? this.queryFormat : arguments.queryFormat;
arguments.params['C0-PARAM0'] = params;
arguments.url = this.url + '?method=' + arguments.params['method'];
arguments.method = 'POST';
arguments.failure = arguments.error;
arguments.timeout = this.timeout;
var ____success = arguments.success;
arguments.success = function(data) {
data = data.responseText.replace(/^\s*|\s*$/g, '');
if (data.substring(0,9) == '__json__:') {
data = Ext.util.JSON.decode(data.slice(9));
}
____success(data, this);
};
if ( params ) {
if (typeof params != 'string') {
arguments.params['C0-PARAM0'] = Ext.util.JSON.encode(params);
}
}
Ext.Ajax.request(arguments);
}
});
Ext.AjaxCFC = new Ext.AjaxCFCConnection();
// Include Ext.AjaxCFC code
<script type="text/javascript" src="js/Ext.AjaxCFC.js"></script>
// Default configuration properties for the ajaxCFC library
__ajaxConfig = {
'url':'/myApp/ajaxCFC/ajax.cfc',
'defaultTimeout':30000,
'beanFactory':'application.beanFactory'
};
// AjaxCFC call using Ext.data.Connection class
Ext.AjaxCFC.request({
bean: 'AColdSpringBean',
method: 'aMethod',
data: {
'id': 416198,
'first_name': 'Steve',
'last_name': 'Brownlee'
},
success: function(details, s){
DataStore.loadData(details);
},
error: function(results){
Ext.MessageBox.alert('Search Failed', 'An unexpected error occurred. Please try again.');
}
});