I am using nested GridPanels inside a parent GridPanel’s RowExpander plugin for quite sometime now. And everything was working wonderfully till recently, when I hit a major hiccup.
As you might be knowing, the RowExpander plugin for the GridPanel might refresh the Expander content for a row/multiple rows on various events, including Sorting of the parent GridPanel, loading of new data in the parent GridPanel’s store, moving columns of the parent GridPanel’s store etc. Any of these events would lead to refresh of the entire view of the GridPanel, thus needing to recreate the Expander content for all rows.
In the event of an updation of a single row, just that row’s corresponding Expander content would need to be recreated.
Now, this might be undesirable in many situations. An example could be when the nested Grids have custom data attached to them. This makes recreating grids almost out of question. In my case, I wanted to preserve the existing Grids for performance and just for the sake of doing it.
A quick Google Search threw up a couple of threads on ExtJs forums, that were of help. In particular, the approach presented in the following thread seemed promising to me:
http://www.extjs.com/forum/showthread.php?10311-Nested-grid-within-Expander-grid
The approach basically captured the html content for the Expander rows in the beforerefresh event for the Grid’s view, and restores the html in the refresh event.
I immediately adapted the code (that extended the RowExpander plugin) provided there by ExtJs community member going by the nickname, willf1976. However, for some reason, the extension was not working for me.
After some analysis, I found that the extension would work only when the entire View refreshes (as it handles only the beforerefresh and refresh events for the Grid’s view). An update for a particular row’s record was not taken into consideration. I was updating a particular record, and which was causing the nested GridPanel to disappear in my case.
So, I made some enhancements to that code. Here’s my extension to the RowExpander to enable it to capture & restore Expander content in all situations:
{syntaxhighlighter brush: jscript;fontsize: 100; first-line: 1; }Ext.grid.RowExpander.prototype.initOrig = Ext.grid.RowExpander.prototype.init;
/**
* extended version of Ext.ux.grid.RowExpander. This version looks for changes in the grid and saves the html of the expanded rows, it then restores the html back into the grid when the view refreshes.
* Note: For this plug in to work properly you need to encapsulate the elements in each expanded row into 1 parent element. Multiple elements at the top of the expanded rows dom element will result in just the first one being captured and preserved.
*/
Ext.override(Ext.grid.RowExpander, {
/**
* @cfg {String|Null} storeHtmlBy
* This is the attribute of the store record that is used to save and restore the html in the grid. If left null then the id of the store record will be used, if you are going to be using remote sorting you may need to change this value. The property of the record you use will need to be a unique identifier. Defaults to null
*/
storeHtmlBy: null,
/**
* @private internal config {Object} storedHtml
* Used to store the html that will be restored to the grid.
*
*/
storedHtml: {},
//Maintains the associations of Record Keys to the Record’s view index before a refresh/update action occurs.
recordKeyAtRow: {},
//Maintains the expanded & rendered statuses for each Record.
recordStatus: {},
//@private
init: function(grid) {
this.initOrig(grid);
if (this.preserveRowsOnRefresh) {
this.grid.on(‘viewready’, this.markRows, this);
this.grid.on(‘rowsinserted’, this.markRows, this);
var view = this.grid.view;
if (typeof Ext.net == undefined && typeof Coolite == undefined && typeof Ext.net.CommandColumn == undefined) {
//Coolite/Ext.net’s CommandColumn also adds this event. So, dont add it again, if the CommandColumn is present.
view.addEvents(“beforerowupdate”);
view.refreshRow = view.refreshRow.createInterceptor(function(record) {
view.fireEvent(“beforerowupdate”, view, view.grid.store.indexOf(record), record);
});
}
view.on(‘beforerefresh’, this.onBeforeRefresh, this);
view.on(‘refresh’, this.onRefresh, this);
view.on(‘beforerowupdate’, this.saveRowHtml, this);
view.on(‘rowupdated’, this.restoreRowHtml, this);
this.on(‘expand’, this.rowExpanded);
this.on(‘collapse’, this.rowCollapsed);
}
},
getRecordKey: function(record) {
if (!Ext.isEmpty(this.storeHtmlBy)) {
return (record.get(this.storeHtmlBy));
} else {
return (record.id);
}
},
rowExpanded: function(expander, record, body, rowIndex) {
var key = this.getRecordKey(record);
var status = { rendered: true, expanded: true };
this.recordStatus[key] = status;
},
rowCollapsed: function(expander, record, body, rowIndex) {
var key = this.getRecordKey(record);
var status = { rendered: true, expanded: false };
this.recordStatus[key] = status;
},
//@private
onBeforeRefresh: function(view) {
var store = this.grid.getStore(),
n, record;
this.storedHtml = {};
for (n = 0; n < store.data.items.length; n++) {
record = store.getAt(n);
this.saveRowHtml(view, n, record);
}
},
saveRowHtml: function(view, index, record) {
var key = this.getRecordKey(record);
var rowIndex = null, found = false;
//Find the view’s rowIndex for this record before the refresh action happened.
//While saving the Row Html for a Record, we have to find the previous position of the record in the view before
//the refresh action happened.
for (rowIndex in this.recordKeyAtRow) {
if (this.recordKeyAtRow[rowIndex] == key) {
found = true;
break;
}
}
if (found) {
var row = view.getRow(rowIndex);
var body = Ext.DomQuery.selectNode(‘div.x-grid3-row-body’, row);
this.storedHtml[key] = body.innerHTML;
}
},
//@private
onRefresh: function(view) {
var store = this.grid.getStore(),
n, row, record;
for (n = 0; n < store.data.items.length; n++) {
row = view.getRow(n);
if (row) {
record = store.getAt(n);
this.restoreRowHtml(view, n, record);
}
}
},
restoreRowHtml: function(view, index, record) {
var key = this.getRecordKey(record);
//When restoring the Row Html for a Record, the restore has to happen at the current position of the Record after
//the refresh action happened.
var storedBody = this.storedHtml[key];
if (!Ext.isEmpty(storedBody)) {
var row = view.getRow(index);
var body = Ext.DomQuery.selectNode(‘div.x-grid3-row-body’, row);
while (body.hasChildNodes()) {
body.removeChild(body.lastChild);
}
var status = this.recordStatus[key];
if (!status) {
//the row has not been touched.
body.innerHTML = this.tpl.html;
this.collapseRow(row);
} else {
body.innerHTML = storedBody;
if (status.expanded)
this.expandRow(row);
else
this.collapseRow(row);
}
}
this.markRow(view, index, record);
},
//@private
markRow: function(view, index, record) {
var key;
var row = view.getRow(index);
if (row) {
key = this.getRecordKey(record);
this.recordKeyAtRow[index] = key;
}
},
markRows: function() {
var view = this.grid.getView(),
store = this.grid.getStore(),
record, n;
this.recordKeyAtRow = {};
for (n = 0; n < store.data.items.length; n++) {
record = store.getAt(n);
this.markRow(view, n, record);
}
}
});{/syntaxhighlighter}
The code is sure lengthy by any standards.
The following are the major interesting features of the above code:
- Instead of creating a new Plugin derived from the RowExpander, the code adds methods to the existing plugin (In my case, I am using this code inside Ext.Net. Extending RowExpander would have forced me to write more routine plumbing code for the server-side which I did not wish to).
- The RowExpander should have a custom config option named preserveRowsOnRefresh and the value of this option should be true for the code to be enabled for that instance of the Expander.
- The code basically captures and restores the Expander html content in various situations, including full view refreshes, as well as a particular row update for the parent Grid.
- You can use the code safely with both ExtJs/Ext.Net. In case of Ext.Net, you only need to make sure that this code is included into the page at an appropriate place. No editing of any server-side or client-side file is needed.
The above code is also attached below. Feel free to adapt and modify it according to your needs. If you face any issues with the code, please use the comment form below to tell me about them.
Credit should go to where its due. A major part of the code has been adpated from this ExtJs thread:
http://www.extjs.com/forum/showthread.php?10311-Nested-grid-within-Expander-grid
In addition, the wonderful Ext.Net core team member vlad, provided some useful insights here:
http://coolite.com/forums/Topic31332-16-1.aspx
When using nested GridPanels or any such ExtJs content inside RowExpander, please take particular care for destroying the nested Grids on events like data reload into store, otherwise you might cause significant memory leaks.
UPDATE:
- (May 3, 2010) – Rewrote the plugin completely for making it to work correctly in all possible updations to the parent grid, which lead to refresh of the Expander rows. Please check this comment below for details and an example.
Hi, Rahul
Thanks for this good idea. I tried your code, but no luck. Any full sample code for nested grid with expander?
Would be appreciated!
And one question: I found ‘beforerefresh’ not always happen, maybe just once. Why not suspend the event from the view object?
But I don’t know the right way to debug and suspend the event, so I am not successful in doing this.
Hi Rahul,
I am using the Parent panel, using the RowExpander Plugin to render the child grids. Now my requirement is to export the grid data in to excel. I have implemented the functionality but it only extracts the parent grid and exports to excel. Child grids are not coming out.
Is there any hook for this to achieve? I have attached part of code snippert for your reference.
Thank you Rahul! Worked like a charm!
Hello Rahul,
I’ve been on this path myself for the last few weeks and am familiar with the ExtJS forum threads you have posted here. They were quite helpful when I was coding the nested row expander.
I took a similar approach here, except that I don’t store the html anywhere. I store a reference to the component in an array (indexed by record.id); just like the original plugin used this.state[record.id] to store the state of the row. During refresh/rowupdated, I just do this:
a) Go through the saved array, and if a row is expanded, I fire expandRow(rowIndex).
b) This triggers the expand listener I’d registered for self. This would look into the array and see if the row already has a component.
c) IF it does then I create a DOM element (a div tag with DomHelper) and append it to the row. Then I pass this to Ext.fly, to get the Ext.Element, on which I call replaceWith(savedRow.getEl()).
i.e.
Ext.fly(‘newDiv’).replaceWith(savedRow.getEl());
This will retain all the state of the internal panel.
This works on Chrome and Firefox. On IE I’m having some odd issues where the saved component seems to ‘forget’ its rendered markup!! The component/element is there in the El cache, but its innerHTML is empty! Trying to debug this now.
Thanks.
The problem here is that when i select a row from a sub-grid, the row from the general grid with the same index also gets selected. (same happens to mouse-over)
note: the reverse does not happen.
Does anybody know how to fix this?
Hi,
I am having trouble displaying the rows in a Grid before expanding it. The rows height is similar to the content before it is actually expanded. If the content inside the expander increases, the row is also displayed with the same height.
It would be really unable for users to determine if a row exists ! Is this a known issue in ExtJS 4.1 or is it fixed ?
Please help !
Hello,
Im novice to EXTJS. i also have a similar requirement where the parent grid values changes accroding to the combo box values selected inside the inner grid. so when i change parent grid value, inner grid is disappearing. I tried to use your plugin. As far as i understood, i need to override the rowExpander plugin and add attribute ‘preserveRowsOnRefresh’ with value true on my plugin. but its not working. Correct me if im wrong. im using extjs 4.2