View Full Version : MultiGroupingGrid = (MultiGroupingStore & MultiGroupingView) with sorting...

18 Apr 2008, 1:38 AM
I hacked around a little and this seems to be working with the few tests I've tried. Hopefully it's useful as a starting point or as a solution.

1)Right now, it only supports sorting all fields in the same direction.(this can easily be modified to allow control of each field's sort direction)
2)You don't have to add the grouping and sorting columns into your ColumnModel...


Demo: http://www.freewebs.com/jokurocks/MultiGroupingGrid.html

Here's an example of how to use it. (Used about the same as the framework's GroupingStore and GroupingView...)

var xg = Ext.grid;

// Array data for the grids
Ext.grid.dummyData = [
['3m Co',71.72,0.02,0.03,'4/2 12:00am', 'Manufacturing'],
['Alcoa Inc',71.72,0.42,1.47,'4/1 12:00am', 'Manufacturing'],
['Altria Group Inc',71.72,0.28,0.34,'4/3 12:00am', 'Manufacturing'],
['American Express Company',52.55,0.01,0.02,'4/8 12:00am', 'Finance'],
['American International Group, Inc.',64.13,0.31,0.49,'4/1 12:00am', 'Services'],
['AT&T Inc.',36.53,-0.48,-1.54,'4/8 12:00am', 'Services'],
['Boeing Co.',71.72,0.53,0.71,'4/8 12:00am', 'Manufacturing'],
['Caterpillar Inc.',36.53,0.92,1.39,'4/1 12:00am', 'Services'],
['Citigroup, Inc.',49.37,0.02,0.04,'4/4 12:00am', 'Finance'],
['E.I. du Pont de Nemours and Company',40.48,0.51,1.28,'4/1 12:00am', 'Manufacturing'],
['Exxon Mobil Corp',68.1,-0.43,-0.64,'4/3 12:00am', 'Manufacturing'],
['General Electric Company',34.14,-0.08,-0.23,'4/3 12:00am', 'Manufacturing'],
['General Motors Corporation',30.27,1.09,3.74,'4/3 12:00am', 'Automotive'],
['Hewlett-Packard Co.',36.53,-0.03,-0.08,'4/3 12:00am', 'Computer'],
['Honeywell Intl Inc',38.77,0.05,0.13,'4/3 12:00am', 'Manufacturing'],
['Intel Corporation',36.53,0.31,1.58,'4/2 12:00am', 'Computer'],
['International Business Machines',81.41,0.44,0.54,'4/1 12:00am', 'Computer'],
['Johnson & Johnson',64.72,0.06,0.09,'4/2 12:00am', 'Medical'],
['JP Morgan & Chase & Co',45.73,0.07,0.15,'4/2 12:00am', 'Finance'],
['McDonald\'s Corporation',36.76,0.86,2.40,'4/2 12:00am', 'Food'],
['Merck & Co., Inc.',40.96,0.41,1.01,'4/2 12:00am', 'Medical'],
['Microsoft Corporation',36.53,0.14,0.54,'4/2 12:00am', 'Computer'],
['Pfizer Inc',27.96,0.4,1.45,'4/8 12:00am', 'Services', 'Medical'],
['The Coca-Cola Company',45.07,0.26,0.58,'4/1 12:00am', 'Food'],
['The Home Depot, Inc.',34.64,0.35,1.02,'4/8 12:00am', 'Retail'],
['The Procter & Gamble Company',61.91,0.01,0.02,'4/1 12:00am', 'Manufacturing'],
['United Technologies Corporation',81.41,0.55,0.88,'4/1 12:00am', 'Computer'],
['Verizon Communications',36.53,0.39,1.11,'4/3 12:00am', 'Services'],
['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'4/3 12:00am', 'Retail'],
['Walt Disney Company (The) (Holding Company)',29.89,0.24,0.81,'4/1 12:00am', 'Services']
// add in some dummy descriptions
for(var i = 0; i < Ext.grid.dummyData.length; i++){
Ext.grid.dummyData[i].push('Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Sed metus nibh, sodales a, porta at, vulputate eget, dui. Pellentesque ut nisl. Maecenas tortor turpis, interdum non, sodales non, iaculis ac, lacus. Vestibulum auctor, tortor quis iaculis malesuada, libero lectus bibendum purus, sit amet tincidunt quam turpis vel lacus. In pellentesque nisl non sem. Suspendisse nunc sem, pretium eget, cursus a, fringilla vel, urna.<br/><br/>Aliquam commodo ullamcorper erat. Nullam vel justo in neque porttitor laoreet. Aenean lacus dui, consequat eu, adipiscing eget, nonummy non, nisi. Morbi nunc est, dignissim non, ornare sed, luctus eu, massa. Vivamus eget quam. Vivamus tincidunt diam nec urna. Curabitur velit.');

// shared reader
var reader = new Ext.data.ArrayReader({}, [
{name: 'company'},
{name: 'price', type: 'float'},
{name: 'change', type: 'float'},
{name: 'pctChange', type: 'float'},
{name: 'lastChange', type: 'date', dateFormat: 'n/j h:ia'},
{name: 'industry'},
{name: 'desc'}

var groupStore = new Ext.ux.MultiGroupingStore({
reader: reader,
data: xg.dummyData,
groupField: ['industry','price'],
sortInfo: {field: 'company', direction: 'DESC'}
var groupView = new Ext.ux.MultiGroupingView({
forceFit: true,
emptyGroupText: 'All Group Fields Empty',
displayEmptyFields: true, //you can choose to show the group fields, even when they have no values
groupTextTpl: '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "Records" : "Record"]})',
displayFieldSeperator: ', ' //you can control how the display fields are seperated

var grid = new xg.GridPanel({
store: groupStore,
columns: [
{id:'company',header: "Company", width: 60, sortable: true, dataIndex: 'company'},
{header: "Price", width: 20, sortable: true, renderer: Ext.util.Format.usMoney, dataIndex: 'price'},
{header: "Change", width: 20, sortable: true, dataIndex: 'change', renderer: Ext.util.Format.usMoney},
{header: "Industry", width: 20, sortable: true, dataIndex: 'industry'},
{header: "Last Updated", width: 20, sortable: true, renderer: Ext.util.Format.dateRenderer('m/d/Y'), dataIndex: 'lastChange'}

view: groupView,

width: 700,
height: 450,
collapsible: true,
animCollapse: false,
title: 'Grouping Example',
iconCls: 'icon-grid',
renderTo: document.body

Here's the MultiGroupingStore...

Ext.ux.MultiGroupingStore = Ext.extend(Ext.data.GroupingStore,{
sortInfo: [],

sort : function(field, dir){
var f = [];

for(i=0,len=field.length; i<len; ++i){

if(f.length < 1){
return false;

if(this.sortInfo && this.sortInfo.length > 0 && this.sortInfo[0].field == f[0].name){ // toggle sort dir
dir = (this.sortToggle[f[0].name] || "ASC").toggle("ASC", "DESC");
dir = f[0].sortDir;

var st = (this.sortToggle) ? this.sortToggle[f[0].name] : null;
var si = (this.sortInfo) ? this.sortInfo : null;

this.sortToggle[f[0].name] = dir;
this.sortInfo = [];
for(i=0,len=f.length; i<len; ++i){
this.sortInfo.push({field: f[i].name, direction: dir});

this.fireEvent("datachanged", this);
if (!this.load(this.lastOptions)) {
if (st) {
this.sortToggle[f[0].name] = st;
if (si) {
this.sortInfo = si;

setDefaultSort : function(field, dir){
dir = dir ? dir.toUpperCase() : "ASC";

this.sortInfo.push({field: field, direction: dir});
for(i=0,len=field.length; i<len; ++i){
this.sortInfo.push({field: field[i].field, direction: dir});
this.sortToggle[field[i]] = dir;

constructor: function(config){

groupBy : function(field, forceRegroup){
if(!forceRegroup && this.groupField == field){
return; // already grouped by this field

this.groupField = field;

this.baseParams = {};
this.baseParams['groupBy'] = field;
var si = this.sortInfo || [];
if(si.field != field){
this.fireEvent('datachanged', this);

applySort : function(){
var si = this.sortInfo;

if(si && si.length > 0 && !this.remoteSort){
this.sortData(si, si[0].direction);

if(!this.groupOnSort && !this.remoteGroup){
var gs = this.getGroupState();
if(gs && gs != this.sortInfo){

getGroupState : function(){
return this.groupOnSort && this.groupField !== false ?
(this.sortInfo ? this.sortInfo : undefined) : this.groupField;

sortData : function(flist, direction){
direction = direction || 'ASC';

var st=[];
var o;
for(i=0,len=flist.length; i < len; ++i){
o = flist[i];
st.push(this.fields.get(o.field ? o.field : o).sortType);

var fn = function(r1, r2){
var v1=[];
var v2=[];
var len = flist.length;
var o;
var name;
for(i=0; i<len; ++i){
o = flist[i];
name= o.field ? o.field : o;


var result;
for(i=0; i<len; ++i){
result = v1[i] > v2[i] ? 1 : (v1[i] < v2[i] ? -1 : 0);
if(result != 0)
return result;

return result; //if it gets here, that means all fields are equal

this.data.sort(direction, fn);
if(this.snapshot && this.snapshot != this.data){
this.snapshot.sort(direction, fn);


Here's the MultiGroupingView...

Ext.ux.MultiGroupingView = Ext.extend(Ext.grid.GroupingView,{
displayEmptyFields: false,
displayFieldSeperator: ', ',

renderRows : function(){
var groupField = this.getGroupField();
var eg = !!groupField;
// if they turned off grouping and the last grouped field is hidden
if(this.hideGroupedColumn) {
var colIndexes = [];
for(i=0,len=groupField.length; i<len; ++i){
if(!eg && this.lastGroupField !== undefined) {
for(i=0,len=this.lastGroupField.length; i<len; ++i){
this.cm.setHidden(this.cm.findColumnIndex(this.lastGroupField[i]), false);
delete this.lastGroupField;
}else if (eg && colIndexes.length > 0 && this.lastGroupField === undefined) {
this.lastGroupField = groupField;
for(i=0,len=colIndexes.length; i<len; ++i){
this.cm.setHidden(colIndexes[i], true);
}else if (eg && this.lastGroupField !== undefined && groupField !== this.lastGroupField) {
for(i=0,len=this.lastGroupField.length; i<len; ++i){
this.cm.setHidden(this.cm.findColumnIndex(this.lastGroupField[i]), false);
this.lastGroupField = groupField;
for(i=0,len=colIndexes.length; i<len; ++i){
this.cm.setHidden(colIndexes[i], true);
return Ext.ux.MultiGroupingView.superclass.renderRows.apply(this, arguments);

doRender : function(cs, rs, ds, startRow, colCount, stripe){
if(rs.length < 1){
return '';
var groupField = this.getGroupField();
this.enableGrouping = !!groupField;

if(!this.enableGrouping || this.isUpdating){
return Ext.grid.GroupingView.superclass.doRender.apply(this, arguments);

var gstyle = 'width:'+this.getTotalWidth()+';';

var gidPrefix = this.grid.getGridEl().id;

var groups = [], curGroup, i, len, gid;
for(i = 0, len = rs.length; i < len; i++)
var rowIndex = startRow + i;
var r = rs[i];

var gvalue=[];
var fieldName;
var grpDisplayValues = [];
var v;
for(j=0,gfLen=groupField.length; j<gfLen; ++j){
fieldName = groupField[j];
v = r.data[fieldName];
grpDisplayValues.push(fieldName + ': ' + v);
else if(this.displayEmptyFields){
grpDisplayValues.push(fieldName + ': ');

//g = this.getGroup(gvalue, r, groupRenderer, rowIndex, colIndexes[index], ds);
if(gvalue.length < 1 && this.emptyGroupText)
g = this.emptyGroupText;
g = grpDisplayValues.join(this.displayFieldSeperator);

if(!curGroup || curGroup.group != g)
gid = gidPrefix + '-gp-' + groupField + '-' + Ext.util.Format.htmlEncode(g);
// if state is defined use it, however state is in terms of expanded
// so negate it, otherwise use the default.
var isCollapsed = typeof this.state[gid] !== 'undefined' ? !this.state[gid] : this.startCollapsed;
var gcls = isCollapsed ? 'x-grid-group-collapsed' : '';
curGroup = {
group: g,
gvalue: gvalue,
text: g,
groupId: gid,
startRow: rowIndex,
rs: [r],
cls: gcls,
style: gstyle
r._groupId = gid;

var buf = [];
for(i = 0, len = groups.length; i < len; i++){
var g = groups[i];
this.doGroupStart(buf, g, cs, ds, colCount);
buf[buf.length] = Ext.grid.GroupingView.superclass.doRender.call(
this, cs, g.rs, ds, g.startRow, colCount, stripe);

this.doGroupEnd(buf, g, cs, ds, colCount);
return buf.join('');

20 Apr 2008, 4:51 PM
This is really nice. Thanks for sharing. The only suggestion I have is that you may want to improve the view display so that it can fold multiple levels.

21 Apr 2008, 7:08 AM
Have a way to provide an example online?

21 Apr 2008, 4:14 PM
jerrybrown5: This is really nice. Thanks for sharing. The only suggestion I have is that you may want to improve the view display so that it can fold multiple levels.

You mean like a hierarchy view with nested grids?

IgorCosta: Have a way to provide an example online?
I just uploaded a demo, the link is on my original post.

22 Apr 2008, 10:03 PM
You mean like a hierarchy view with nested grids?

Have you ever used excel's multiple level grouping feature set? They are pretty much the gold standard on anything similar to this. A nested hierachial view with multiple levels of nested rows yes...nested grids no...It is much simpler than this. You will just need to use properly indent the expansion and contraction icons. Cheers again on your nice addition.

Best regards,
Jerry Brown