1. #1
    Ext User
    Join Date
    Sep 2007
    Posts
    102
    Vote Rating
    0
    timo.nuros is on a distinguished road

      0  

    Default [2.0] Ext.ux.grid.SubTableRowExpander: Foldable Rows for Grid

    [2.0] Ext.ux.grid.SubTableRowExpander: Foldable Rows for Grid


    Hi,

    this is a Plugin which extends the example grid3 row extender.

    Notes:
    - The data you read via the store needs to have a field where your sub-records are located in. You need to have exactly the same columns in your sub-records as in your main ones. This means: If you have the columns "company", "value" and "date" in your main records, you also need to have the columns "company", "value" and "date" in your sub-records.
    - You need to pass the name of that field using the config field 'subdata'. If you don't specify a field name, it defaults to 'subdata'.
    - All records are displayed in the expanded row as seen below in the screenshot. Right now, this is only a display plugin; it won't work with an EditorGrid.
    - The plugin re-renders and re-builds the template whenever you hide or show columns and whenever you resize columns. Do not use this with a huge amount of columns! This plugin is meant to be used with a PagingToolbar. Feel free to implement caching yourself
    - You can see how the data for the subtable is stored in the example. If you use the ArrayReader, specify 'array' for the reader value, otherwise 'json' is specified. 'json' is also the default value.

    Known Issues:
    - Column Alignment might be off some pixels. I have no clue why, and I really don't have the time to dig that deep into Ext's CSS code.

    Other notes:
    - I cannot provide support for this one. Feel free to report bugs anyways, maybe I'll fix them - but don't expect that.
    - If you fix a bug, post it here so I can put it together for other users.

    Example code:
    Code:
    /*
     * Ext JS Library 2.0.2
     * Copyright(c) 2006-2008, Ext JS, LLC.
     * licensing@extjs.com
     * 
     * http://extjs.com/license
     */
    
    
    Ext.onReady(function(){
    
        Ext.QuickTips.init();
        
        var xg = Ext.grid;
    
        // 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: 'subdata'},
           {name: 'desc'}
    	   
        ]);
    
        ////////////////////////////////////////////////////////////////////////////////////////
        // Grid 1
        ////////////////////////////////////////////////////////////////////////////////////////
        // row expander
        var expander = new Ext.ux.SubTableRowExpander({
    		subdata: 'subdata',
    		reader: 'array'
        });
    
    	expander.on("subdblclick", function (r) { alert(r.data.company); });
    	
        var grid1 = new xg.GridPanel({
            store: new Ext.data.Store({
                reader: reader,
                data: xg.subRowDummyData
            }),
            cm: new xg.ColumnModel([
                expander,
                {id:'company',header: "Company", width: 40, 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'},
                {header: "% Change", width: 20, sortable: true, dataIndex: 'pctChange'},
                {header: "Last Updated", width: 20, sortable: true, renderer: Ext.util.Format.dateRenderer('m/d/Y'), dataIndex: 'lastChange'}
            ]),
            viewConfig: {
                forceFit:true
            },
            width: 600,
            height: 300,
            plugins: expander,
            collapsible: true,
            animCollapse: false,
            title: 'Expander Rows, Collapse and Force Fit',
            iconCls: 'icon-grid',
            renderTo: document.body
        });
    
    });
    
    Ext.grid.subRowDummyData = [
        ['3m Co',71.72,0.02,0.03,'9/1 12:00am', 'Manufacturing', 
    		[
    			['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am', 'Manufacturing'],
    			['AT&T Inc.',31.61,-0.48,-1.54,'9/1 12:00am', 'Services'],
    			['E.I. du Pont de Nemours and Company',40.48,0.51,1.28,'9/1 12:00am', 'Manufacturing']
    		]
    	],
        ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am', 'Manufacturing', 
    		[
    			['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am', 'Manufacturing'],
    			['AT&T Inc.',31.61,-0.48,-1.54,'9/1 12:00am', 'Services'],
    			['E.I. du Pont de Nemours and Company',40.48,0.51,1.28,'9/1 12:00am', 'Manufacturing']
    		]
    	],
        ['Altria Group Inc',83.81,0.28,0.34,'9/1 12:00am', 'Manufacturing', 
    		[
    			['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am', 'Manufacturing'],
    			['AT&T Inc.',31.61,-0.48,-1.54,'9/1 12:00am', 'Services'],
    			['E.I. du Pont de Nemours and Company',40.48,0.51,1.28,'9/1 12:00am', 'Manufacturing']
    		]
    	],
        ['American Express Company',52.55,0.01,0.02,'9/1 12:00am', 'Finance', 
    		[
    			['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am', 'Manufacturing'],
    			['AT&T Inc.',31.61,-0.48,-1.54,'9/1 12:00am', 'Services'],
    			['E.I. du Pont de Nemours and Company',40.48,0.51,1.28,'9/1 12:00am', 'Manufacturing']
    		]
    	]
    ];
    Plugin code:
    Code:
    Ext.grid.RowExpander = function(config){
        Ext.apply(this, config);
    
        this.addEvents({
            beforeexpand : true,
            expand: true,
            beforecollapse: true,
            collapse: true
        });
    
        Ext.grid.RowExpander.superclass.constructor.call(this);
    
        if(this.tpl){
            if(typeof this.tpl == 'string'){
                this.tpl = new Ext.Template(this.tpl);
            }
            this.tpl.compile();
        }
    
        this.state = {};
        this.bodyContent = {};
    };
    
    Ext.extend(Ext.grid.RowExpander, Ext.util.Observable, {
        header: "",
        width: 20,
        sortable: false,
        fixed:true,
        menuDisabled:true,
        dataIndex: '',
        id: 'expander',
        lazyRender : true,
        enableCaching: true,
    
        getRowClass : function(record, rowIndex, p, ds){
            p.cols = p.cols-1;
            var content = this.bodyContent[record.id];
            if(!content && !this.lazyRender){
                content = this.getBodyContent(record, rowIndex);
            }
            if(content){
                p.body = content;
            }
    		
            return this.state[record.id] ? 'x-grid3-row-expanded' : 'x-grid3-row-collapsed';
        },
    
        init : function(grid){
            this.grid = grid;
    
            var view = grid.getView();
            view.getRowClass = this.getRowClass.createDelegate(this);
    
            view.enableRowBody = true;
    
            grid.on('render', function(){
                view.mainBody.on('mousedown', this.onMouseDown, this);
            }, this);
        },
    
        getBodyContent : function(record, index){
            if(!this.enableCaching){
                return this.tpl.apply(record.data);
            }
            var content = this.bodyContent[record.id];
            if(!content){
                content = this.tpl.apply(record.data);
                this.bodyContent[record.id] = content;
            }
            return content;
        },
    
        onMouseDown : function(e, t){
            if(t.className == 'x-grid3-row-expander'){
                e.stopEvent();
                var row = e.getTarget('.x-grid3-row');
                this.toggleRow(row);
            } 
        },
    
        renderer : function(v, p, record){
            p.cellAttr = 'rowspan="2"';
            return '<div class="x-grid3-row-expander">&#160;</div>';
        },
    
        beforeExpand : function(record, body, rowIndex){
            if(this.fireEvent('beforeexpand', this, record, body, rowIndex) !== false){
                if(this.tpl && this.lazyRender){
                    body.innerHTML = this.getBodyContent(record, rowIndex);
                }
    			
    	           return true;
            }else{
                return false;
            }
        },
    
        toggleRow : function(row){
            if(typeof row == 'number'){
                row = this.grid.view.getRow(row);
            }
            this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'expandRow' : 'collapseRow'](row);
        },
    	
        expandRow : function(row){
            if(typeof row == 'number'){
                row = this.grid.view.getRow(row);
            }
            var record = this.grid.store.getAt(row.rowIndex);
            var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
            if(this.beforeExpand(record, body, row.rowIndex)){
                this.state[record.id] = true;
                Ext.fly(row).replaceClass('x-grid3-row-collapsed', 'x-grid3-row-expanded');
                this.fireEvent('expand', this, record, body, row.rowIndex);
            }
        },
    
        collapseRow : function(row){
            if(typeof row == 'number'){
                row = this.grid.view.getRow(row);
            }
            var record = this.grid.store.getAt(row.rowIndex);
            var body = Ext.fly(row).child('tr:nth(1) div.x-grid3-row-body', true);
            if(this.fireEvent('beforcollapse', this, record, body, row.rowIndex) !== false){
                this.state[record.id] = false;
                Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed');
                this.fireEvent('collapse', this, record, body, row.rowIndex);
            }
        }
    });
    
    Ext.ux.SubTableRowExpander = function(config){
    	if (!config.subdata) { config.subdata = 'subdata'; }
        Ext.apply(this, config);
    
        Ext.ux.SubTableRowExpander.superclass.constructor.call(this);
    
        this.state = {};
        this.bodyContent = {};
    	
    };
    
    
    Ext.extend(Ext.ux.SubTableRowExpander, Ext.grid.RowExpander, {
    	enableCaching: false,
    	
    	init: function (grid) {
    		
    		 this.addEvents({
            	dblclick: true,
    			mouseover: true,
    			mouseout: true,
    			subdblclick: true
    			});
    			
    	    var ret = Ext.ux.SubTableRowExpander.superclass.init.call(this, grid);
    		
    		this.grid.view.afterMethod('onColumnHiddenUpdated', this.reconfigureTemplate, this);
    		this.grid.view.afterMethod('onLayout', this.reconfigureTemplate, this);
    		this.grid.view.afterMethod('onColumnWidthUpdated', this.doWidth, this);
    		this.grid.view.afterMethod('onAllColumnWidthsUpdated', this.doAllWidths, this);
    		this.grid.view.afterMethod('afterMove', this.doAllWidths, this);
    		
    		this.grid.on('dblclick', this.onDblClick, this);
    		
    		this.grid.store.on('load', function (store, records) { this.createSubdata(); }.createDelegate(this));
    		this.createSubdata();
    		
    		this.on("expand", function (e, record) {
    			var res = Ext.query("table.x-grid3-row-subtable");
    			
    			for (var i=0;i<res.length;i++) {
    				
    				Ext.fly(res[i]).on("mouseover", this.onMouseOver);
    				Ext.fly(res[i]).on("mouseout", this.onMouseOut);
    			}
    			
    			for (var i=0;i<record.subdata.records.length;i++) {
    
    				Ext.fly("subtable-" + records.records[i].id).on("mouseover", this.onMouseOver);
    				Ext.fly("subtable-" + records.records[i].id).on("mouseout", this.onMouseOut);
    			}
    			
    			
    			
    		});
    		
    	},
        renderer : function(v, p, record){
           	p.cellAttr = 'rowspan="2"';
           	return '<div class="x-grid3-row-expander">&#160;</div>';
        },
    	createSubdata: function () {
    		
    		switch (this.reader) {
    			case "array":
    				var reader = new Ext.data.ArrayReader({}, this.grid.store.reader.recordType);
    				break;
    			case "json":
    			default:
    				var reader = new Ext.data.JsonReader({}, this.grid.store.reader.recordType);
    				break;
    		}
    		
    		for (j=0;j<this.grid.getStore().getCount();j++) {
    			var record = this.grid.getStore().getAt(j);
    
    			if (record.data[this.subdata] && record.data[this.subdata].length > 0) {
    				record.subdata = this.processRenderMethod(reader.readRecords(record.data[this.subdata]));
    			} else {
    				record.subdata = {};
    				record.subdata.records = new Array();
    			}
    		}
    	},
    	doWidth: function () {
    		this.reconfigureTemplate();
    		this.updateRows();
    	},
    	onDblClick : function(e){
    		var t = e.target;
    
    		if (Ext.fly(t.id)) {
    			var target = Ext.fly(t.id);
    			if (target.hasClass("x-grid3-cell-subtablerow")) {
    				
    				var parent = target.findParent("tr.x-grid3-subtable-outertable-row", 10);
    				
    				var parent2 = target.findParent(this.grid.view.rowSelector, 20);
    				var record = this.grid.store.getAt(parent2.rowIndex).subdata.records[parent.rowIndex];
    
    				this.fireEvent("subdblclick", record);
    			}
    		}
        },
    	onMouseOver: function (e) {
    		var t = e.target;
    
    		if (Ext.fly(t.id)) {
    			var target = Ext.fly(t.id);
    			if (target.hasClass("x-grid3-cell-subtablerow")) {
    
    				var parent = target.findParent("table.x-grid3-row-subtable", 10, true);
    				parent.addClass("x-grid3-row-over");
    			}
    		}
    	},
    	onMouseOut: function (e) {
    		var t = e.target;
    
    		if (Ext.fly(t.id)) {
    			var target = Ext.fly(t.id);
    			if (target.hasClass("x-grid3-cell-subtablerow")) {
    				var parent = target.findParent("table.x-grid3-row-subtable", 10, true);
    				parent.removeClass("x-grid3-row-over");
    
    			}
    		}
    	},
    	fly : function(el){
            if(!this._flyweight){
                this._flyweight = new Ext.Element.Flyweight(document.body);
            }
            this._flyweight.dom = el;
            return this._flyweight;
        },
    	updateRow: function (row) {
    		var record = this.grid.store.getAt(row);
    
    	    if(typeof row == 'number'){
    	        row = this.grid.view.getRow(row);
    	    }
    	    
    	    var body = Ext.DomQuery.selectNode('tr:nth(2) div.x-grid3-row-body', row);
    		
    		records = record.subdata;
    
    		var content = this.tpl.apply(records.records);
    		
    		this.bodyContent[record.id] = content;
    	   	body.innerHTML = content;
    		
    	},
    	updateRows: function () {
          	var ns = this.grid.view.getRows();
            for(var i = 0, len = ns.length; i < len; i++){
    			this.updateRow(i);
    		}
    	},
    	doAllWidths: function () {
    		this.reconfigureTemplate();
    		this.updateRows();
    	},	
    	getActivatedGridColumns: function () {
    		var cm = this.grid.getColumnModel();
    		
    		var cols = [];
    		
    		
    		
    		for(var i = 0; i < cm.getColumnCount(); i++){
    			if (!cm.isHidden(i)) {
    				var col = {};
    				var name = cm.getDataIndex(i);
    				col.name = (typeof name == 'undefined' ? this.ds.fields.get(i).name : name);
    				col.index = i;
    				col.width = this.getColumnWidth(i);
    				col.renderer = cm.getRenderer(i);
    				cols.push(col);
    			}
    		}
    		
    		return cols;
    	},
    	getColumnWidth : function(col){
    		var cm = this.grid.getColumnModel();
    		
            var w = cm.getColumnWidth(col);
            if(typeof w == 'number'){
                w = (Ext.isBorderBox ? w : (w-this.grid.view.borderWidth > 0 ? w-this.grid.view.borderWidth:0));
            }
    		
            return w;
        },
    		
    	reconfigureTemplate: function () {
    		var cols = this.getActivatedGridColumns();
    
    		var template = [
    							'<table class="x-grid3-subtable-outertable" cellspacing="0">',
    							'<tpl for=".">'
    						];
    
    		
    		
    		var padding, colWidth;
    		
    		template.push('<tr class="x-grid3-subtable-outertable-row"><td><table cellspacing="0" id="subtable-{values.id}" cellpadding="0" class="x-grid3-row-subtable"><tr class="x-grid3-row x-grid3-subtable-row-alt">');
    		
    		for(var i = 1; i < cols.length; i++){
    			
    			
    			if (i == 1) {
    				padding = '';
    			} else {
    				padding = '';
    			}
    		
    			colWidth = cols[i].width;
    			template.push('<td class="x-grid3-col x-grid3-cell x-grid3-td-'+cols[i].name+'" style="width: '+colWidth+'px;"><div style="'+padding+'" unselectable="on" id="subfield-{values.id}-'+cols[i].name+'" class="x-grid3-cell-subtablerow x-grid3-cell-inner x-grid3-col-'+cols[i].name+'">' + '{values.data.' + cols[i].name + '}</div></td>');
    		}
    		
    		template.push('</tr>');
    		template.push('</table></td></tr></tpl></table>');
    		
    		this.tpl = new Ext.XTemplate(template);
    					
    	    if(this.tpl){
    	        if(typeof this.tpl == 'string'){
    	            this.tpl = new Ext.Template(this.tpl);
    	        }
    	        this.tpl.compile();
    	    }
    				
    		return;
    		
    	},
    	processRenderMethod: function (records) {
    		var cols = this.getActivatedGridColumns();
    
    		for (var i=0;i<records.records.length;i++) {
    			for(var j = 1; j< cols.length; j++){
    				records.records[i].data[cols[j].name] = cols[j].renderer(records.records[i].data[cols[j].name],
    																		 {},
    																		 records.records[i]);
    			}
    		}
    		
    		return records;
    	},
        getBodyContent : function(record, index){
    		records = record.subdata;
    		
    		var body = "";
    				
    		if (records.records.length > 0) {
    			body = this.tpl.apply(records.records);
    		}
    		this.bodyContent[record.id] = body;
    		
    		return body;
        }
    });
    Attached Images

  2. #2
    jay@moduscreate.com's Avatar
    Join Date
    Mar 2007
    Location
    Frederick MD, NYC, DC
    Posts
    16,353
    Vote Rating
    77
    jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all jay@moduscreate.com is a name known to all

      0  

    Default


    i like it. i also like how you give copyright over to ext for the code

  3. #3
    Ext User
    Join Date
    Sep 2007
    Posts
    102
    Vote Rating
    0
    timo.nuros is on a distinguished road

      0  

    Default


    It's because these files are from the example, and I didn't remove the copyright notice. License for my extension is as-is.

  4. #4
    Ext User
    Join Date
    Mar 2008
    Posts
    89
    Vote Rating
    0
    Shaguar is on a distinguished road

      0  

    Default


    Can you please post a comment how to use this plugin with MySQL and JSON/PHP to fill the grid instead of using a fix array of data?
    That would be awesome!
    Thx.

  5. #5
    Ext User
    Join Date
    Sep 2007
    Posts
    102
    Vote Rating
    0
    timo.nuros is on a distinguished road

      0  

    Default


    1. Fill the grid using JSON like you would usually do.
    2. Specify "reader" : "json" in the config (see above).

  6. #6
    Ext User
    Join Date
    Mar 2008
    Posts
    89
    Vote Rating
    0
    Shaguar is on a distinguished road

      0  

    Default


    Thx, another question:
    How can i hide the expander icon "+" if the row doesnt have any sub entries?

  7. #7
    Ext User
    Join Date
    Sep 2007
    Posts
    102
    Vote Rating
    0
    timo.nuros is on a distinguished road

      0  

    Default


    Quote Originally Posted by Shaguar View Post
    Thx, another question:
    How can i hide the expander icon "+" if the row doesnt have any sub entries?
    Not possible at the moment. Feel free to contribute a patch.

    Timo

  8. #8
    Ext User
    Join Date
    Mar 2008
    Posts
    24
    Vote Rating
    0
    vk214 is on a distinguished road

      0  

    Default


    Timo, this is a great extension! One question, would your extension for the Row Expander work for an XML reader? I'm getting data from the back-end in XML format, not in JSON Array format. I was thinking of adding a 'subdata' node in my tree of data; but not sure how to implement your extension with this...

    Thanks in advance.

  9. #9
    Ext User
    Join Date
    Sep 2007
    Posts
    102
    Vote Rating
    0
    timo.nuros is on a distinguished road

      0  

    Default


    Have a look at the source; there's a location where a JsonReader is created. Add your reader to that and give it a try. I didn't add it because I never used XMLReader.

    Regards,
    Timo

  10. #10
    Ext User
    Join Date
    Mar 2008
    Posts
    24
    Vote Rating
    0
    vk214 is on a distinguished road

      0  

    Default


    I'm a bit of a beginner, so this question may seem trivial - But, in your sample code, you have:
    Code:
    var expander = new Ext.ux.SubTableRowExpander({
    		subdata: 'subdata',
    		reader: 'array'
        });
    In my case, what would take the place of 'array' ? Is that the type of reader you are using? I've tried 'xml' or 'xmlReader' but neither seem to work.

    Also, maybe you can point me to where the reader is defined in the source code, I can't seem to find it? Did you mean to look in RowExpander.js, and your extension code?

    Thanks...
    Last edited by mystix; 3 Apr 2008 at 7:21 PM. Reason: use [code][/code] tags