You are hereBlogs / rahul's blog / ExtJs - Preserving RowExpander markup across View Refreshes
ExtJs - Preserving RowExpander markup across View Refreshes
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:
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);
}
}
});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.
| Attachment | Size |
|---|---|
| ExtendedRowExpander.js.txt | 5.77 KB |







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 Ling, I was able to reproduce some issues with the above plugin. Specifically, it did not work correctly while sorting the parent grid.
I had to rewrite 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.
I have attached a complete example with this comment. Can you please test it and report back if it works successfully for you. I hope it would. I will be waiting for your feedback before updating the code in the main blog post.
I am using your Expander.aspx as a guideline for my own nested grid code. It has been a big help. However I have found that after restoreRowHtml() is called the first time, the nested grid is no longer swallowing the events. My workaround is to have beforeRowExpand() call swallowEvent() every time:
var beforeRowExpand = function (expander, record, body, rowIndex)
{
var rowView = this.grid.getView().getRow(rowIndex);
var rowViewObject = Ext.get(rowView);
var expanderContent = rowViewObject.child('.row-expander-box');
if (!record.rendered) {
var grd = getSiteGrid(record.data.appid);
grd.render(expanderContent);
//grd.getEl().swallowEvent(['mousedown', 'mouseup', 'click', 'contextmenu', 'mouseover', 'mouseout', 'dblclick', 'mousemove']);
record.rendered = true;
}
var gridEl = expanderContent.child('.x-grid-panel');
gridEl.swallowEvent(['mousedown', 'mouseup', 'click', 'contextmenu', 'mouseover', 'mouseout', 'dblclick', 'mousemove']);
}
This prevents the events from bubbling up to the parent grid. However, I am running into additional problems. After restoreRowHtml() is called the first time, I cannot check the checkboxes or select rows in my nested grids. I would appreciate any insight you might be able to offer.
Hi Will, I am currently running very short on time, and might not be able to test your issue. However, I recall that I faced many issues trying to preserve RowExpander markup across View refreshes (I had complex GridPanel inside the RowExpander), and I actually ended up re-creating the Expander content each time a row/view changed. And believe me, recreating content took less time than the hassles involved in trying to peserve the markup.
I don't know if the content of Expander.aspx is still relevant or likely to be used/updated, but I noticed that
Hi Jon, I still use the plugin in a couple of places where RowExpander content is not that complex, but for a nested GridPanel scenario, I have resorted instead to recreating grids on view refresh.
Regarding typeof evaluating to string "undefined", you can try changing the same in the if condition also. I think you are using the plugin outside Ext.Net and in that case, its important for that condition to evaluate to true and the subsequent code getting executed.
Ling, regarding your other point about suspending the 'beforerefresh' event, first, I am not aware of any built-in ExtJs support for suspending Component events selectively. Either you suspend all events or you suspend none.
Moreover, I would recommend against doing so while using this plugin, because the beforerefresh event is vital for the Plugin to capture the Html for nested grids that it restores later.
As far as I can see, 'beforerefresh' event always fires whenever it is supposed to as per ExtJs documentation.
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.
Hi Soundararajan, I had a very quick look at your code and could not figure out where are you exporting data to Excel.
Regardless, I think you are traversing over the data items of the main Grid store. You need to explicitly traverse the child grid store too for each row and export the data to excel for child store items as required.
Hi Rahul,
Thanks for your reply. I have uploaded the some part of code only. Please find the attached screenshots and the full code for your reference.
The problem which i am facing here is. I have the full access to main grid. But subgrids are generated by calling expander plugin from maingrid.If so , to render the child data, i need to click the expandable symbol every time.
My requirement is , with out clicking expander i need to extract the child grid data. For that the key which i have is the Parent Record id. But how to pass that record id from Parent grid.
Hi soundarrajan, I am really short of time to look at such lengthy piece of code. I would repeat what I said above. In your following piece of code:
exportToExcelGridPanel.getStore().each(function(rec) { var idx = exportToExcelGridPanel.getStore().indexOf(rec); var rowArray=[]; for(i=0; i<visibleColumnCnt;i++) { rowArray[i]=rec.get(columnModel.getDataIndex(i+1))+EMPTY_STRING; if(rowArray[i].indexOf("a>") !=-1) { rowArray[i] = removeHyperLink(rowArray[i]); } rowArray[i] = removeUnwantedCharacters(rowArray[i]); } excelXmlData.append(getPartialExcelXml(rowArray,rec,"row","cell")); }, this);You need to ensure that each rowArray element gets nested grid's data too.
EDIT: After reading your comment again, I recalled I earlier had a similar situation. My solution was to create all nested Grids and Stores beforehand when data is loaded for parent grid (instead of creating nested grids in row expand listener for Row Expander).
The nested grids and stores were pushed to an array indexed with record ids for the parent grid. And then, while exporting, I accessed the required grid from the array with the record ids fetched from parent grid's store.
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.
Hi Eashwar, thanks for the detailed reply. I do not recall correctly but you would have instances where the component would "forget" its rendered html in other browsers also (I think I had it in Chrome Frame, which prompted me to save html).
In any case, I will like to mention that after some time I wrote this plugin, I myself ceased to use it. I found a better and more maintainable way of doing it.
Allow the nested grids to be destroyed in rowupdate/refresh, but preserve the nested grid's store. Create a new GridPanel with that store and render the new gridpanel to the row.
I will try to write a blog entry for that aspect too soon.
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?
Post new comment