PDA

View Full Version : [3.x, 3.2.0] RowExpander plugin UPDATED - April 5, 2010



daiei27
5 Apr 2010, 2:31 PM
History
August 10, 2010
- Added toolbar handling as an example to destroyNestedExtCmp (code below is highlighted in red)
- Also, if you use the Header for RowExpander extension, see post #10 below...
April 5, 2010

- Initial release
--

NOTE to all RowExpander plugin users:
If you use Ext components within expanded rows and are not aware of the memory leak issue, CHECK FOR THEM!

As a quick check, add this link to your page and keep checking the value as you expand/collapse various components.

<a href="javascript: alert(Ext.ComponentMgr.all.length)" style="color:gray; font-size:smaller">Check ExtJS resources</a>--
This update was made to address that issue, starting from Mikhail's modified RowExpander (http://www.extjs.com/forum/showthread.php?p=416604#post416604) and running ExtJS 3.2.0.

I added support to clean up any type of Ext component. I mainly added the cleanup of Ext Panels which covered most of my stuff (charts, etc.), but made it extremely easy for you to add any specific component needs of your own.

The previous code did not clean up multiple components at the same level so I modified it to cover that scenario.

It appears to work well in my simple tests (a grid with rows containing multiple charts and multiple grids, with those containing multiple charts and panels in them).

Cheers,
Chris

--
Attached and posted here:

/*!
* Ext JS Library 3.2.0
* Copyright(c) 2006-2009 Ext JS, LLC
* licensing@extjs.com
* http://www.extjs.com/license
*
* Modifications by Mykhail Stadnik <http://mikhailstadnik.com>:
* - Nesting grids support added
* Modifications by Chris Newman
* - 04/05/2010
* - Added support for Ext components in general
* - Added support for multiple components at the same level
* - Memory-leak fix for Panel-related components
* - NOTE: see destroyNestedExtCmp below for adding your own component handling
* - 08/10/2010
* - As an example, added toolbar to destroyNestedExtCmp
*/
Ext.ns( 'Ext.ux.grid');

/**
* @class Ext.ux.grid.RowExpander
* @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.RowExpander = 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,
actAsTree : false,
treeLeafProperty : 'is_leaf',
appendRowClass : true,

constructor: function( config){
if (!config.id) {
config.id = Ext.id();
}

Ext.apply( this, config);

var css =
'.x-' + this.id + '-grid3-row-collapsed .x-grid3-row-expander { background-position:0 0; }' +
'.x-' + this.id + '-grid3-row-expanded .x-grid3-row-expander { background-position:-25px 0; }' +
'.x-' + this.id + '-grid3-row-collapsed .x-grid3-row-body { display:none !important; }' +
'.x-' + this.id + '-grid3-row-expanded .x-grid3-row-body { display:block !important; }' +
'.x-grid-expander-leaf .x-grid3-row-expander { background: none; }'
;

Ext.util.CSS.createStyleSheet( css, Ext.id());

this.expanderClass = 'x-grid3-row-expander';
this.rowExpandedClass = 'x-' + this.id + '-grid3-row-expanded';
this.rowCollapsedClass = 'x-' + this.id + '-grid3-row-collapsed';
this.leafClass = 'x-grid-expander-leaf';

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.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 = {};
},

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;
}
var cssClass = this.state[record.id] ? this.rowExpandedClass : this.rowCollapsedClass;
if (this.actAsTree && record.get( this.treeLeafProperty)) {
cssClass = this.leafClass;
}
return cssClass;
},

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.on( 'destroy', this.onDestroy, this);

view.on( 'beforerefresh', this.onBeforeRefresh, this);
view.on( 'refresh', this.onRefresh, this);
},

// @private
onRender: function() {
var grid = this.grid;
var mainBody = grid.getView().mainBody;
mainBody && mainBody.on( 'mousedown', this.onMouseDown, this, {delegate: '.' + this.expanderClass});
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);
}
if (this.actAsTree) {
/**
* Stop bubbling parent events
*/
grid.getEl().swallowEvent([ 'mouseover', 'mouseout', 'mousedown', 'click', 'dblclick' ]);
}
},

// @private
onBeforeRefresh : function() {
var rows = this.grid.getEl().select( '.' + this.rowExpandedClass);
rows.each( function( row) {
this.collapseRow( row.dom);
}, this);
},

// @private
onRefresh : function() {
var rows = this.grid.getEl().select( '.' + this.rowExpandedClass);
rows.each( function( row) {
Ext.fly( row).replaceClass( this.rowExpandedClass, this.rowCollapsedClass);
}, this);
},

// @private
onDestroy: function() {
this.keyNav.disable();
delete this.keyNav;
var mainBody = this.grid.getView().mainBody;
mainBody && mainBody.un( 'mousedown', this.onMouseDown, this);
},

// @private
onRowDblClick: function( grid, rowIdx, e) {
this.toggleRow(rowIdx);
},

onEnter: function( e) {
var g = this.grid;
var sm = g.getSelectionModel();
var sels = sm.getSelections();
for (var i = 0, len = sels.length; i < len; i++) {
var rowIdx = g.getStore().indexOf(sels[i]);
this.toggleRow(rowIdx);
}
},

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){
e.stopEvent();
var row = e.getTarget( '.x-grid3-row');
this.toggleRow(row);
},

renderer : function(v, p, record){
p.cellAttr = 'rowspan="2"';
return '<div class="' + this.expanderClass + '"> </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);
}
if (Ext.fly(row).hasClass( this.leafClass)) {
return ;
}
this[Ext.fly(row).hasClass( this.rowCollapsedClass) ? 'expandRow' : 'collapseRow'](row);
},

expandRow : function( row){
if(typeof row == 'number'){
row = this.grid.view.getRow( row);
}
if (Ext.fly(row).hasClass( this.leafClass)) {
return ;
}
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( this.rowCollapsedClass, this.rowExpandedClass);
this.fireEvent( 'expand', this, record, body, row.rowIndex);
}
},

/**
* Avoid memory leaks by destroying all nested grids recursively
* - Added handling of multiple children and panels (for charts)
* @param {Ext.Element} - grid element to destroy
*/
destroyNestedExtCmp : function( gridRow) {
var gridEl;
var bSearch = 1;
while (bSearch) {
bSearch = 0;
// Grids
if (gridEl = gridRow.child('.x-grid-panel')) {
this.destroyNestedExtCmp( gridEl);
var grid = Ext.getCmp( gridEl.id);
if (grid && (grid != this.grid)) {
if (grid instanceof Ext.grid.EditorGridPanel) {
var cm = grid.getColumnModel();
for (var i = 0, s = cm.getColumnCount(); i < s; i++) {
for (var ii = 0, ss = grid.getStore().getCount(); ii < ss; ii++) {
if (editor = cm.getCellEditor( i, ii)) {
editor.destroy();
}
}
}
cm.destroy();
}
grid.destroy();
bSearch = 1;
}
}
// Panels, toolbars, etc.
// (x-panel covers many other components that come wrapped in panels, including charts)
if ((gridEl = gridRow.child('.x-panel')) ||
(gridEl = gridRow.child('.x-toolbar')) ) { // <-- Add simple ExtJS component here
var cmp = Ext.getCmp( gridEl.id); // Grab component
cmp.destroy(); // Destroy component
bSearch = 1; // Search for more
}
/**
*** ADD more complex ExtJS component handling requirements here!!!
**/
} ;// end while - Ext components are found
},

collapseRow : function( row){
if (typeof row == 'number'){
row = this.grid.view.getRow( row);
}
if (Ext.fly( row).hasClass( this.leafClass)) {
return ;
}
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.destroyNestedExtCmp( Ext.get( row));
if (record) this.state[record.id] = false;
Ext.fly( row).replaceClass( this.rowExpandedClass, this.rowCollapsedClass);
this.fireEvent( 'collapse', this, record, body, row.rowIndex);
}
}
});

Ext.preg( 'rowexpander', Ext.ux.grid.RowExpander);

//backwards compatibility
Ext.grid.RowExpander = Ext.ux.grid.RowExpander;

daiei27
12 Apr 2010, 8:45 AM
Hmm.. looks like the view count on the zip file resets when I update it. Anyways...

For those that downloaded, any issues/updates?

Sesshomurai
14 Apr 2010, 8:04 AM
I'll try this when I get home. I use an old version. But one question I had was. How easy would it be to modify it so it can load content into the expanded section via some ajax call? Any tips before I start coding that?

thanks and nice work.

lingz_public
26 Apr 2010, 10:43 PM
Thanks daiei, will check my expander...
Just curious whether you are successful to put one gridpanel inside expander? The problem is that the internal gridpanel behaves wired, and also triger collapse of outside gridpanel.
If you have a workable example for this, would be great!

daiei27
4 Jun 2010, 2:29 PM
Sorry. Somehow I missed these posts...


I'll try this when I get home. I use an old version. But one question I had was. How easy would it be to modify it so it can load content into the expanded section via some ajax call? Any tips before I start coding that?

thanks and nice work.
I load some stuff via AJAX, but mostly it's taking advantage of the AJAX mechanisms of the stores linked to my nested grids. For your problem, sounds like you just have to make your own AJAX call in the listeners property.

var resultExpander = new xg.RowExpander({
actAsTree : true,
treeLeafProperty : 'is_leaf',
listeners : {
expand : function( expander, record, body, rowIndex) {
// YOUR AJAX FUNCTION HERE
showTcResults(record.data.id,record.data.testcase); // loads my nested grids
}
},
tpl : new Ext.Template(
'<div class="dropdown ux-row-expander-box">',
' <div style="color:red; font-weight:bold">What sort of result info you want here?</div><br />',
' <div class="heading">Result files</div>',
' <div class="body">{resultfiles}</div>',
' <div class="heading">Detailed results</div>',
' <div class="body" id="results_tc_{id}"></div>',
' <div class="body" id="results_tc_graph_{id}"></div>',
'</div>'
)
}); // end resultExpander
That's a snippet from my code to give you an idea of where to put it.


Thanks daiei, will check my expander...
Just curious whether you are successful to put one gridpanel inside expander? The problem is that the internal gridpanel behaves wired, and also triger collapse of outside gridpanel.
If you have a workable example for this, would be great!
Unfortunately I do not have a demo I can post online, but this does work. I needed (or I should say REALLY WANTED ;) ) a gridpanel nested within a rowExpander. Mikhail had the closest thing to it with some minor issues. That's why I took his code and expanded on it to get rid of the remaining memory leaks (that I saw) and to allow things like charts and other types of panels.

daiei27
4 Jun 2010, 2:34 PM
I should mention I searched all over the place for this sort of functionality. RowPanelExpander appeared to be the closest thing, but it did not handle a lot of the nesting issues that Mikhail led the charge on sorting out. If anyone finds a better solution, I'd be more than happy to hear it!

Patches and improvements for RowExpander plugin are also welcome! :D

mjmonserrat
16 Jun 2010, 4:43 AM
How to add Form on RowExpander wherein the text boxes and the check boxes inside the form serves as the Row Editor?

cq.yangyu@gmail.com
18 Jun 2010, 1:40 AM
Is there a demo for us ?

daiei27
10 Aug 2010, 10:00 AM
How to add Form on RowExpander wherein the text boxes and the check boxes inside the form serves as the Row Editor?
You can tackle that all sorts of ways. You could actually create some sort of custom form in the RowExpander and update your stores as users apply changes, but it sounds like you may just want a RowEditor sort of plugin?

Is there a demo for us ?
See post #5. ;)

daiei27
10 Aug 2010, 11:38 AM
If you use the Header for RowExpander extension (http://www.sencha.com/forum/showthread.php?92958-Header-for-RowExpander), I found a memory leak when you open a row, then expand all rows. On expansion, the row's objects appear to be recreated, but the previous ones not properly destroyed.

Here's my code with the fix in red:

= new xg.RowExpander({
header: '<div class="x-grid3-hd-row-expander" style="display:inline;padding-left:18px;margin-left: -1px;"> </div>',
// Override init function to add on header click behavior
init : xg.RowExpander.prototype.init.createSequence(function(grid) {
grid.on('render', function() {
// add on header click behavior
Ext.fly(grid.getView().innerHd).on('mousedown', this.onHdMouseDown, this);
grid.getStore().on('load', function() {
var hdEl = Ext.fly(grid.getView().innerHd).child('div.x-grid3-hd-expander');
if (hdEl && hdEl.hasClass('x-grid3-hd-expanded')) {
hdEl.replaceClass('x-grid3-hd-expanded', 'x-grid3-hd-collapsed');
}
}, this);
}, this);
}),
// process on header click event
onHdMouseDown : function(e, t) {
if (t.className == 'x-grid3-hd-row-expander') {
e.stopEvent();
var hd = Ext.fly(t.parentNode);
var isChecked = hd.hasClass('x-grid3-hd-expanded');
if (isChecked) {
hd.replaceClass('x-grid3-hd-expanded', 'x-grid3-hd-collapsed');
this.collapseAll();
} else {
var mask = new Ext.LoadMask(this.grid.id);
mask.show();
hd.replaceClass('x-grid3-hd-collapsed', 'x-grid3-hd-expanded');
var thisVar = this;
function expandRows () {
thisVar.expandAll();
mask.hide();
}
setTimeout(expandRows, 100);
}
}
},
// Expand all rows
expandAll : function() {
for (var i = 0; i < this.grid.getStore().getCount(); i++) {
this.collapseRow(i);// workaround to prevent memory leak from expanding an expanded row
this.expandRow(i);
}
},
// Collapse all rows
collapseAll : function() {
for (var i = 0; i < this.grid.getStore().getCount(); i++) {
this.collapseRow(i);
}
},
// For DUT-DP table
listeners : {
expand : function( expander, record, body, rowIndex) {
showSesToolbar(record.data.session);
}
},
tpl : new Ext.Template(
'<div class="dropdown">',
' <div id="toolbar_{session}"></div><br />',
'</div>'
)
})
Expansion took a while before adding the additional collapse anyways so I added a mask while it loads. If you find that useful, look for the code in blue. :)

boriss
15 Aug 2010, 6:37 AM
In line 203 (in onDestroy) there is an if missing. Comparing the Ext JS 3.2.1 version of the RowExpander with yours I think the onDestroy function should look like this:


if (this.keyNav) {
this.keyNav.disable();
delete this.keyNav;
}
var mainBody = this.grid.getView().mainBody;
mainBody && mainBody.un( 'mousedown', this.onMouseDown, this);

daiei27
3 Dec 2010, 7:59 AM
It might have been deliberate, but I'll look into it. Thanks for the heads up.

MrSparks
11 Jun 2011, 11:37 PM
@daiei27,

Wondered if you have an 4.x compatible version of this updated UX?

daiei27
16 Jun 2011, 5:30 PM
I haven't had time to upgrade yet...

Maybe someone could help if you post some code snippets and/or whatever errors you were seeing...

Soumya_Jagalur
21 Jan 2013, 4:00 AM
Hi Anybody please post the upgraded version of Rowexpander plugin . The one which is there in Sencha docs have some issue. Please post the upgraded for ext4.0