1. #1
    Sencha User steffenk's Avatar
    Join Date
    Jul 2007
    Location
    Haan, Germany
    Posts
    2,656
    Vote Rating
    6
    steffenk has a spectacular aura about steffenk has a spectacular aura about steffenk has a spectacular aura about

      0  

    Default RowPanelExpander for grids

    RowPanelExpander for grids


    This allows to use normal panels in RowExpander. I found the idea somewhere in the net and integrated it in RowExpander.

    Online-Demo

    Updated 23/12/2010: make it stateful, corrected other mentioned issues, added expandAll and collapsAll methods



    Here is the plugin:
    Code:
    Ext.ns('Ext.ux.grid');
    
    /**
     * @class Ext.ux.grid.RowPanelExpander
     * @extends Ext.util.Observable
     * Plugin (ptype = 'rowexpander') that adds the ability to have a Column in a grid which enables
     * a second row body which expands/contracts.  The expand/contract behavior is configurable to react
     * on clicking of the column, double click of the row, and/or hitting enter while a row is selected.
     *
     * @ptype rowexpander
     */
    Ext.ux.grid.RowPanelExpander = Ext.extend(Ext.util.Observable, {
    	/**
    	 * @cfg {Boolean} expandOnEnter
    	 * <tt>true</tt> to toggle selected row(s) between expanded/collapsed when the enter
    	 * key is pressed (defaults to <tt>true</tt>).
    	 */
    	expandOnEnter : true,
    	/**
    	 * @cfg {Boolean} expandOnDblClick
    	 * <tt>true</tt> to toggle a row between expanded/collapsed when double clicked
    	 * (defaults to <tt>true</tt>).
    	 */
    	expandOnDblClick : true,
    
    	header : '',
    	width : 20,
    	sortable : false,
    	fixed : true,
    	menuDisabled : true,
    	dataIndex : '',
    	id : 'expander',
    	lazyRender : true,
    	enableCaching : true,
    
    	constructor : function(config) {
    		Ext.apply(this, config);
    
    		this.addEvents( {
    			/**
    			 * @event beforeexpand
    			 * Fires before the row expands. Have the listener return false to prevent the row from expanding.
    			 * @param {Object} this RowExpander object.
    			 * @param {Object} Ext.data.Record Record for the selected row.
    			 * @param {Object} body body element for the secondary row.
    			 * @param {Number} rowIndex The current row index.
    			 */
    			beforeexpand : true,
    			/**
    			 * @event expand
    			 * Fires after the row expands.
    			 * @param {Object} this RowExpander object.
    			 * @param {Object} Ext.data.Record Record for the selected row.
    			 * @param {Object} body body element for the secondary row.
    			 * @param {Number} rowIndex The current row index.
    			 */
    			expand : true,
    			/**
    			 * @event beforecollapse
    			 * Fires before the row collapses. Have the listener return false to prevent the row from collapsing.
    			 * @param {Object} this RowExpander object.
    			 * @param {Object} Ext.data.Record Record for the selected row.
    			 * @param {Object} body body element for the secondary row.
    			 * @param {Number} rowIndex The current row index.
    			 */
    			beforecollapse : true,
    			/**
    			 * @event collapse
    			 * Fires after the row collapses.
    			 * @param {Object} this RowExpander object.
    			 * @param {Object} Ext.data.Record Record for the selected row.
    			 * @param {Object} body body element for the secondary row.
    			 * @param {Number} rowIndex The current row index.
    			 */
    			collapse : true
    		});
    
    		Ext.ux.grid.RowPanelExpander.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 = {};
    	},
    
    	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', this.onRender, this);
    		grid.store.on('load', this.onStoreLoaded, this);
    		grid.on('destroy', this.onDestroy, this);
    		grid.on("beforestaterestore", this.applyState, this);
    		grid.on("beforestatesave", this.saveState, this);
    	},
    
    	// @private
    	onRender : function() {
    		var grid = this.grid;
    
    
    		var mainBody = grid.getView().mainBody;
    		mainBody.on('mousedown', this.onMouseDown, this, {
    			delegate : '.x-grid3-row-expander'
    		});
    
    		grid.getView().on('rowremoved', this.onRowRemoved, this);
    		grid.getView().on('rowupdated', this.onRowUpdated, this);
    
    		if (this.expandOnEnter) {
    			this.keyNav = new Ext.KeyNav(this.grid.getGridEl(),
    					{
    						'enter' : this.onEnter,
    						scope : this
    					});
    		}
    		if (this.expandOnDblClick) {
    			grid.on('rowdblclick', this.onRowDblClick, this);
    		}
    	},
    
    	onStoreLoaded: function(store, records,options) {
    		var index = -1;
    		for(var key in this.state){
    			if (this.state[key] === true) {
    				index = store.indexOfId(key);
    				if (index > -1) {
    					this.expandRow(index);
    				}
    			}
    		}
    	},
    
    	/** @private */
    	applyState: function(grid, state){
    		this.suspendStateStore = true;
    		if(state.expander) {
    			this.state = state.expander;
    		}
    		this.suspendStateStore = false;
    	},
    
    	/** @private */
    	saveState: function(grid, state){
    		return state.expander = this.state;
    	},
    
    	/** @private */
    	onDestroy : function() {
    		if (this.keyNav) {
    			this.keyNav.disable();
    			delete this.keyNav;
    		}
    		/*
    		 * A majority of the time, the plugin will be destroyed along with the grid,
    		 * which means the mainBody won't be available. On the off chance that the plugin
    		 * isn't destroyed with the grid, take care of removing the listener.
    		 */
    		var mainBody = this.grid.getView().mainBody;
    		if (mainBody) {
    			mainBody.un('mousedown', this.onMouseDown, this);
    		}
    	},
    
    	/** @private */
    	onRowDblClick : function(grid, rowIdx, e) {
    		this.toggleRow(rowIdx);
    	},
    
    
    		// This will not get fired for an update
    	onRowRemoved: function(view, row, rec) {
    		var panelItemIndex = rec.id;
    
    		if (this.expandingRowPanel && this.expandingRowPanel[panelItemIndex]) {
    			this.expandingRowPanel[panelItemIndex].destroy();
    			this.expandingRowPanel[panelItemIndex] = null;
    		}
    	},
    
    	onRowUpdated: function(view, row, rec) {
    		if (typeof row == 'number') {
    			row = this.grid.view.getRow(row);
    		}
    
    		this[Ext.fly(row).hasClass('x-grid3-row-collapsed') ? 'collapseRow' : 'expandRow'](row);
    	},
    
    	getBodyContent : function(record, index) {
    		// extend here
    		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) {
    		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);
    			}
    			if (body.innerHTML == '' || !this.enableCaching) {
    				this.createExpandingRowPanel(record, body, 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.grid.saveState();
    			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('beforecollapse', this, record, 	body, row.rowIndex) !== false) {
    			this.state[record.id] = false;
    			Ext.fly(row).replaceClass('x-grid3-row-expanded', 'x-grid3-row-collapsed');
    			this.grid.saveState();
    			this.fireEvent('collapse', this, record, body, row.rowIndex);
    		}
    	},
    
    		// Expand all rows
    	expandAll : function() {
    		var aRows = this.grid.getView().getRows();
    		for (var i = 0; i < aRows.length; i++) {
    			this.expandRow(aRows[i]);
    		}
    	},
    
    		// Collapse all rows
    	collapseAll : function() {
    		var aRows = this.grid.getView().getRows();
    		for (var i = 0; i < aRows.length; i++) {
    			this.collapseRow(aRows[i]);
    		}
    	},
    
    	createExpandingRowPanel : function(record, rowBody, rowIndex) {
    		// record.id is more stable than rowIndex for panel item's key; rows can be deleted.
    		var panelItemIndex = record.id;
    		// var panelItemIndex = rowIndex;
    
    		// init array of expanding row panels if not already inited
    		if (!this.expandingRowPanel) {
    			this.expandingRowPanel = [];
    		}
    
    		// Destroy the existing panel if present
    		if (this.expandingRowPanel[panelItemIndex]) {
    			this.expandingRowPanel[panelItemIndex].destroy();
    		}
    		this.expandingRowPanel[panelItemIndex] = new Ext.Panel({
    			border : false,
    			bodyBorder : false,
    			layout : 'form',
    			renderTo : rowBody,
    			items : this.createExpandingRowPanelItems(record, rowIndex)
    		});
    
    	},
    
    	/**
    	 * Override this method to put Ext form items into the expanding row panel.
    	 * @return Array of panel items.
    	 */
    	createExpandingRowPanelItems : function(record, rowIndex) {
    		var panelItems = [];
    
    		return panelItems;
    	}
    });
    
    Ext.preg('rowexpander', Ext.ux.grid.RowPanelExpander);
    Here is a possible usage of the plugin:
    Code:
    this.expander = new Ext.ux.grid.RowPanelExpander({
    	createExpandingRowPanelItems: function(record, rowIndex){
    		var panelItems = [
    			new Ext.TabPanel({
    				plain: true,
    				activeTab: 0,
    				defaults: {
    					autoHeight: true
    				},
    				record: record,
    				items:[
    					{
    						title:'Info',
    						listeners: {
    							activate: function(panel) {
    								TYPO3.EM.ExtDetails.showExtInfo(panel, panel.ownerCt.record.data);
    							}
    						}
    					},
    					{
    						title:'Update',
    						html: '<div class="loading-indicator">Loading...</div>',
    						disabled: record.data.installed == 0,
    						listeners: {
    							activate: function(panel) {
    								TYPO3.EM.ExtDirect.getExtensionUpdate(record.data.extkey, function(response) {
    									panel.update(response);
    								});
    							}
    						}
    					},
    					{
    						title:'Configuration',
    						disabled: record.data.installed == 0,
    					},
    					{
    						title:'Files',
    						xtype: 'extfilelist'
    					},
    					{
    						title:'Upload to TER'
    					},
    					{
    						title:'Backup/Delete',
    						disabled: record.data.installed == 0,
    					}
    				]
    			})
    		];
    		return panelItems;
    	}
    });
    and it looks like on screenshot.

    When using it with stateful grid, don't forget to add expand and collapse to the stateEvents.

    have fun!
    Attached Images
    vg Steffen
    --------------------------------------
    Release Manager of TYPO3 4.5

  2. #2
    Sencha User
    Join Date
    Feb 2009
    Posts
    20
    Vote Rating
    0
    gregoire is on a distinguished road

      0  

    Default nice

    nice


    Very nice. I can see where this could be very useful.

  3. #3
    Sencha User
    Join Date
    Nov 2007
    Posts
    50
    Vote Rating
    0
    gurpal2000 is on a distinguished road

      0  

    Default


    How do you adapt this to the RowExpander example on the ExtJS 3 examples page?
    im seeing:

    this.addEvents is not a function
    [Break on this error] collapse : true

    thanks

  4. #4
    Sencha User steffenk's Avatar
    Join Date
    Jul 2007
    Location
    Haan, Germany
    Posts
    2,656
    Vote Rating
    6
    steffenk has a spectacular aura about steffenk has a spectacular aura about steffenk has a spectacular aura about

      0  

    Default


    Hi,

    maybe you forgot to use it in grids plugins?
    this.plugins[this.expander]

    I will make a online demo soon.
    vg Steffen
    --------------------------------------
    Release Manager of TYPO3 4.5

  5. #5
    Sencha User
    Join Date
    Nov 2007
    Posts
    50
    Vote Rating
    0
    gurpal2000 is on a distinguished road

      0  

    Default


    yes i forgot the 'new' keyword! but when i click + (expand) nothing happens.

    heres the code

    Code:
    Ext.onReady(function() {
    	
    	Ext.QuickTips.init();
    
    	var dummyData = [
    	    ['3m Co',71.72,0.02,0.03,'9/1 12:00am'],
    	    ['Alcoa Inc',29.01,0.42,1.47,'9/1 12:00am'],
    	    ['The Coca-Cola Company',45.07,0.26,0.58,'9/1 12:00am'],
    	    ['The Home Depot, Inc.',34.64,0.35,1.02,'9/1 12:00am'],
    	    ['The Procter & Gamble Company',61.91,0.01,0.02,'9/1 12:00am'],
    	    ['United Technologies Corporation',63.26,0.55,0.88,'9/1 12:00am'],
    	    ['Verizon Communications',35.57,0.39,1.11,'9/1 12:00am'],
    	    ['Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am'],
    	    ['Walt Disney Company (The) (Holding Company)',29.89,0.24,0.81,'9/1 12:00am']
    	];
    	
    	// add in some dummy descriptions
    	for(var i = 0; i < dummyData.length; i++){
    	    dummyData[i].push('Lorem ipsum dolor sit amet, consectetuer adipiscing elit. ');
    	}
    
    	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: 'desc'}
        ]);
        
        var expander = new Ext.ux.grid.RowPanelExpander({
    			createExpandingRowPanelItems: function(record, rowIndex) {
    				var panelItems = [
    							new Ext.TabPanel({
    								plain: true,
    								activeTab: 0,
    								defaults: {
    									autoHeight: true
    								},
    								record: record,
    								items:[
    									{
    										title:'Info'
    									},
    									{
    										title:'Info2'
    									}
    								]
    							})
    						];
    						return panelItems;
    			}
    		});
    
        var grid = new Ext.grid.GridPanel({
            store: new Ext.data.Store({
                reader: reader,
                data: dummyData
            }),
            cm: new Ext.grid.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,
            frame: true,
            expander: expander,
            title: 'Test row panel expander',
            renderTo: document.body
        });
    });

  6. #6
    Ext User tonedeaf's Avatar
    Join Date
    Dec 2007
    Posts
    137
    Vote Rating
    1
    tonedeaf is on a distinguished road

      0  

    Default


    @gurpal2000
    Your code still does not contain a plugins: config for the grid.

  7. #7
    Sencha User
    Join Date
    Nov 2007
    Posts
    50
    Vote Rating
    0
    gurpal2000 is on a distinguished road

      0  

    Default


    d'oh! solved by the below. For some reason i had "expander: expander" in my code

    Code:
    plugins: [ expander ]

  8. #8
    Sencha User steffenk's Avatar
    Join Date
    Jul 2007
    Location
    Haan, Germany
    Posts
    2,656
    Vote Rating
    6
    steffenk has a spectacular aura about steffenk has a spectacular aura about steffenk has a spectacular aura about

      0  

    Default


    Here is the Online-Demo
    vg Steffen
    --------------------------------------
    Release Manager of TYPO3 4.5

  9. #9
    Sencha User
    Join Date
    Mar 2007
    Posts
    44
    Vote Rating
    0
    killfill is on a distinguished road

      0  

    Default


    hey steffenk in the grid of that screenshot you seem to use a GridFilter that is reendered in the grid header. How are you doing that?

    Thanks!

  10. #10
    Sencha User steffenk's Avatar
    Join Date
    Jul 2007
    Location
    Haan, Germany
    Posts
    2,656
    Vote Rating
    6
    steffenk has a spectacular aura about steffenk has a spectacular aura about steffenk has a spectacular aura about

      0  

    Default


    This is the SearchField included in examples/ux, which i changed not to reload the store but to use filterBy.
    vg Steffen
    --------------------------------------
    Release Manager of TYPO3 4.5