I am creating a page which will dynamically generate collapsed panels. When a user expands these panels, it will perform a GET request and populate this generated panel with the JSON response. The idea is to perform a sort of lazy-load or as-needed load, as the amount of data that would be shown initially can get overwhelming.
However, I can't seem to get the listeners for my panels to work.
Here is the code, which generates the panels through a button's click function:
xtype : 'button',
listeners : {
click : function (button, e, eOpts) {
console.log("Click function");
Ext.Ajax.request({
url: 'data/Countries.json',
success: function(response, options) {
var data = Ext.JSON.decode(response.responseText).results;
var container = Ext.getCmp('panelContainer');
container.removeAll();
for (i = 0; i < data.length; i++)
{
container.add({
xtype: 'panel',
title: 'Country Name - ' + data[i].countryName,
collapsible: true,
listeners: {
expand: function() {
Ext.Ajax.request({
url: 'data/CountryData.json',
success: function(response, options) {
var data = Ext.JSON.decode(response.responseText).results;
var me = this;
me.add({
xtype: 'grid',
store: Ext.create('Ext.data.Store',
{
fields : [{
name: 'gdp'
}, {
name: 'rank'
}, {
name: 'founded'
}, {
name: 'governor'
}, {
name: 'notes'
}], //eo fields
data: data.information,
}),// eo store
columns: [
{ text: 'GDP', dataIndex: 'gdp'},
{ text: 'rank', dataIndex: 'rank'},
{ text: 'Date', dataIndex: 'founded'},
{ text: 'name', dataIndex: 'governor'},
{ text: 'Notes', dataIndex: 'notes', flex: 1, cellWrap: true}
], //eo columns
autoLoad: true
});
},
failure: function(response, options) {}
});
},
collapse: function() {
console.log("Collapse function");
var me = this;
me.removeAll();
}
}//eo panel listeners
});//eo cont.add()
}//eo for loop
}, //eo success
failure: function(response, options) {
//HTTP GET request failure
}//eo failure
});//eo Ajax request
} //eo click
}//eo button listeners
Originally, the panels were dynamically generated along with their populated grids from the click event, which worked perfectly. By wrapping the grid creation in a listener on the dynamically generated panel to create a load-as-needed, I can't get the expand or collapse listeners to trigger.
Searching around, one possible solution I haven't tried is to create a custom component and call it through its xtype rather than build everything in-line, which would let me define listeners there instead of nesting them in a function (this is better as well for readable and reusable code, but I'm just trying to get to the root of the issue for now).
Is there an issue with listeners on dynamically generated panels? What is the reason that the event triggers for collapse and expand aren't firing?
Thanks for all the help!
I'm still have a few issues, but as my main question was about firing the listeners, I'll write the solution I reached.
The issue I had was getting listeners to fire in a dynamically generated element. This led to nested listener functions, and I hadn't defined a scope. I had tried pagep's solution of setting the defaultListenerScope, but for me personally I didn't see a change.
I instead wrapped the listener functions into their own functions and called then through the listener like this:
listeners: {
expand: 'expandFunction',
collapse: 'collapseFunction'
},//eo panel listeners
expandFunction: function() {
//Do Ajax request and add grid to panel
},
collapseFunction: function() {
//Remove all child elements from this panel
}
Instead of doing this:
listeners: {
expand: function() {
//Do Ajax request and add grid to panel
},
collapse: function() {
//Remove all child elements from this panel
}
},//eo panel listeners
By wrapping the info this way, I was able (to a certain degree) to remove the nesting of listeners with generated elements. I also created a custom component and placed these listeners with the component I was generating. My only issue now is populating the generated element, since I am getting Uncaught TypeError: Cannot read property 'add' of undefined when trying to reference the itemId of my component.
My final simplified code, which generates a collapsed panel on button-click and populates it with generated data when expanded, looks like this:
//View.js
click: function (button, e, eOpts) {
Ext.Ajax.request({
url: 'data/Countries.json',
success: function(response, options) {
var data = Ext.JSON.decode(response.responseText).results;
var container = Ext.getCmp('panelContainer');
console.log(container);
container.removeAll();
for (i = 0; i < data.length; i++)
{
container.add({
xtype: 'customPanel',
title: data[i].country
});
}
});
//customPanel.js
Ext.define('MyApp.view.main.CustomPanel', {
extend: 'Ext.panel.Panel',
alias: 'widget.customPanel',
xtype: 'panel',
collapsible: true,
collapsed: true,
listeners: {
expand: 'expandFunction',
collapse: 'collapseFunction'
},//eo panel listeners
expandFunction: function() {
//Do Ajax request and add grid to panel
},
collapseFunction: function() {
//Remove all child elements from this panel
}
});
I have a Kendo HierarchicalDataSource object bound to a Kendo treeview widget.
The HierarchicalDataSource simply returns a one-level-deep json formatted object, but for some reason it won't render in the treeview. It just shows the top node "Dimensions", but renders no data when expanded.
Here is my plunk treeview sample , which contains index.html and script.js .
FYI for script.js :
$scope.dimenDataSource is the Kendo HierarchicalDataSource object which uses the transport property to call my method getDimensionsFromServer2 and also specify the schema.
Another FYI: In getDimensionsFromServer2() I have two ways of returning my test data. The dataFlat var returns a flat array, which renders fine. The data object has nested data, but does NOT render in treeview.
I'm not sure what's going wrong.
Thank you,
Bob
**** UPDATE ****
The problem was the incorrect placement of the schema setting (see my answer):
settings.dimenDataSource = new kendo.data.HierarchicalDataSource({
transport: {
read: function(options){
datacontext.getDimensionsFromServer().then(function (data) {
var rootnode = [{ name: "Dimensions", items: data.data }];
options.success(rootnode);
});
},
schema: {
model: { children: "items" }
},
loadOnDemand: false
}
});
My mistake was in the schema placement, which I had inadvertently placed in the transport option. It should placed at the same level, not within it.
Here's is the corrected version:
settings.dimenDataSource = new kendo.data.HierarchicalDataSource({
transport: {
read: function(options){
datacontext.getDimensionsFromServer().then(function (data) {
var rootnode = [{ name: "Dimensions", items: data.data }];
options.success(rootnode);
});
},
loadOnDemand: false
},
schema: {
model: { children: "items" }
}
});
I am pretty new to Backbone and Backbone.Marionette. I succeed to create a simple page with a sort of datagrid that allows me paging (first, previous, next, last pages), quick searching (triggered at each time a key is pressed), choosing the number of items shown on page (5, 10, all, ...)
Now that I have something working, I tried to improve that and to make these features as a sort of reusable component but I do not know exactly the way to follow. I do not know how to start to refine the work already done.
For example, I want to be able to change the collection/model manage by the datagrid without rewriting everything. This is where I am not confident how to do that and it is probably due to a lack of knowledge. So your inputs and advice to go further will be really appreciated and welcomed.
// JST and HAML Assets is used for the templating pre-compilation
Backbone.Marionette.Renderer.render = function(template, data) {
if (!JST[template]) {
throw "Template '" + template + "' not found!";
}
return JST[template](data);
};
window.MyApp = new Backbone.Marionette.Application();
MyApp.addRegions({
content: ".content-box"
});
MyApp.Datagrid = (function() {
var Datagrid, ItemPerPageView, Layout, PagerView, QuickSearchView, Theme, ThemeView, Themes, ThemesView;
Datagrid = {};
Layout = Backbone.Marionette.Layout.extend({
template: "layouts/grid",
regions: {
grid: "#grid",
quickSearch: "#quickSearch",
itemPerPage: "#itemPerPage",
pager: ".pager"
}
});
Theme = Backbone.Model.extend();
Themes = Backbone.ExtendedCollection.paginatedCollection.extend({
url: "/themes",
model: Theme,
initialize: function() {
var _this = this;
MyApp.vent.on("quickSearch:term", function(term) {
_this.quickSearch(term);
});
MyApp.vent.on("itemPerPage:count", function(count) {
_this.perPage(count);
});
MyApp.vent.on("pager:previous", function() {
_this.previous();
});
MyApp.vent.on("pager:next", function() {
_this.next();
});
MyApp.vent.on("pager:first", function() {
_this.first();
});
MyApp.vent.on("pager:last", function() {
_this.last();
});
}
});
ThemeView = Backbone.Marionette.ItemView.extend({
tagName: "tr",
template: "theme",
model: Theme,
events: {
"click span": "edit",
"blur input": "save"
},
edit: function(event) {
var id, span;
id = this.model.get("id");
span = $("span", this.el).hide();
$("input", this.el).show().focus().val(span.text());
},
save: function(event) {
var id, input, span;
id = this.model.get("id");
span = $("span", this.el).show();
input = $("input", this.el).hide();
if (this.model.get("name") !== input.val()) {
this.model.set("name", input.val());
this.model.save();
}
span.text(this.model.get("name"));
}
});
ThemesView = Backbone.Marionette.CompositeView.extend({
template: "index",
model: Theme,
itemView: ThemeView,
collection: Themes,
itemViewContainer: "#themes",
serializeData: function() {
return this.data;
}
});
QuickSearchView = Backbone.Marionette.View.extend({
el: "#quickSearch",
events: {
"keyup input": "search"
},
search: function(event) {
var searchTerm;
searchTerm = this.$("input").val().trim();
MyApp.vent.trigger("quickSearch:term", searchTerm);
}
});
ItemPerPageView = Backbone.Marionette.View.extend({
el: "#itemPerPage",
events: {
"change select": "count"
},
count: function(event) {
var count;
count = this.$("select").val();
MyApp.vent.trigger("itemPerPage:count", count);
}
});
PagerView = Backbone.Marionette.View.extend({
el: ".pager",
events: {
"click #next": "next",
"click #previous": "previous",
"click #first": "first",
"click #last": "last"
},
first: function(event) {
MyApp.vent.trigger("pager:first");
},
last: function(event) {
MyApp.vent.trigger("pager:last");
},
next: function(event) {
MyApp.vent.trigger("pager:next");
},
previous: function(event) {
MyApp.vent.trigger("pager:previous");
}
});
Datagrid.initializeLayout = function() {
var collection;
Datagrid.layout = new Layout();
Datagrid.layout.on("show", function() {
MyApp.vent.trigger("layout:rendered");
});
MyApp.content.show(Datagrid.layout);
collection = new Themes();
collection.fetch();
collection.on("reset", function() {
return Datagrid.layout.grid.show(new ThemesView({
collection: collection
}));
});
};
MyApp.vent.on("layout:rendered", function() {
var itemPerPageView, pagerView, quickSearchView;
quickSearchView = new QuickSearchView();
Datagrid.layout.quickSearch.attachView(quickSearchView);
itemPerPageView = new ItemPerPageView();
Datagrid.layout.itemPerPage.attachView(itemPerPageView);
pagerView = new PagerView();
Datagrid.layout.pager.attachView(pagerView);
});
return Datagrid;
})();
MyApp.addInitializer(function() {
MyApp.Datagrid.initializeLayout();
});
$(document).ready(function() {
return MyApp.start();
});
Edit 1:
Based on the answer given and my own ideas, I wrote a first draft of a solution. I did not succeed to write a real reusable component but I have a solution that consolidate my code. Some part need to be refactored and improved. There are also some pitfals that I want to solve in a later refactoring.
To add some context, the application is written with Rails as the backend. So there is my javascript folder structure
assets
|--javascripts
|--application.js
|--admin
|--admin.js
|--admin.layout.js
|--subthemes
|--admin.subtheme.controller.js
|--admin.subtheme.view.js
|--themes
|--admin.theme.controller.js
|--admin.theme.view.js
|--templates
|--admin
|--subthemes
|--index.hamlc
|--subtheme.hamlc
|--themes
|--index.hamlc
|--theme.hamlc
|--layouts
|--grid.hamlc
First, the application.js start. The assets pipelines from Rails 3.2 will prepare the dependencies as expected:
//= require underscore
//= require backbone
//= require backbone.marionette
//= require_tree ./lib/backbone
//= require hamlcoffee
//= require i18n
//= require i18n/translations
//= require_tree ../templates/
//= require_tree ./admin
//= require_tree ./admin/theme
//= require_tree ./admin/subtheme
I18n.defaultLocale = "en";
Backbone.Marionette.Renderer.render = function(template, data) {
if (!JST[template]) {
throw "Template '" + template + "' not found!";
}
return JST[template](data);
};
$(document).ready(function() {
return MyApp.start();
});
Now, we can prepare the admin part to start:
var AdminRouter, TempView;
// Create the application for admin part
MyApp.Admin = new Backbone.Marionette.Application();
// Define a router to handle the grid collection type change
AdminRouter = Backbone.Marionette.AppRouter.extend({
initialize: function() {
var _this = this;
// Route quite generic to easily change the data in the grid
this.route(/^admin\/(.*?)$/, "changeCollection");
// Manage event to handle the navigation on client side
MyApp.Admin.vent.on("admin:navigate", function(link) {
_this.navigate(link, {
trigger: true
});
});
},
// Trigger an event to change the collection if one exist for the URL
changeCollection: function(collectionName) {
MyApp.Admin.vent.trigger("grid:collection:change", collectionName);
}
});
// Side menu that allows changing the collection in the data grid
SideMenuView = Backbone.Marionette.View.extend({
el: ".side-menu",
events: {
"click a": "handleClick"
},
// Prevent the normal behavior on the link click
handleClick: function(event) {
event.preventDefault();
MyApp.Admin.vent.trigger("admin:navigate", $(event.target).attr("href"));
}
});
// Add the initializer to the main application to prepare the admin part (grid)
MyApp.addInitializer(function() {
new SideMenuView();
new AdminRouter();
Backbone.history.start({
pushState: true
});
MyApp.Admin.start();
});
Then we can define the datagrid part:
// This the grid layout module in the admin namespace
MyApp.Admin.module("GridLayout", function(GridLayout, Admin, Backbone, Marionette, $, _) {
var ItemPageSelectorView, Layout, PagerView, QuickSearchView;
// The quick search view handle the related fields to do the quick search
QuickSearchView = Backbone.Marionette.View.extend({
el: ".gridQuickSearch",
events: {
"keyup input": "search"
},
// Get the field content and trigger an event with it
search: function(event) {
var searchTerm;
searchTerm = $(event.target).val().trim();
$("input", this.$el).val(searchTerm);
Admin.vent.trigger("grid:quickSearch:term", searchTerm);
}
});
// The item page selecto handle the choice of how many rows should be displayed per page
ItemPageSelectorView = Backbone.Marionette.View.extend({
el: ".gridItemPageSelector",
events: {
"change select": "count"
},
// Get the number of items per page that should be displayed
count: function(event) {
var count;
count = $(event.target).val();
$("select", this.$el).val(count);
Admin.vent.trigger("grid:itemPageSelector:count", count);
}
});
// The pager view manage the view components to change the page shown in the data grid
PagerView = Backbone.Marionette.View.extend({
el: ".gridPager",
events: {
"click #next": "next",
"click #previous": "previous",
"click #first": "first",
"click #last": "last",
"click #page": "page"
},
//
// The following functions triggers events to go to the right pages
//
first: function(event) {
Admin.vent.trigger("grid:pager:first");
},
previous: function(event) {
Admin.vent.trigger("grid:pager:previous");
},
page: function(event) {
Admin.vent.trigger("grid:pager:page");
},
next: function(event) {
Admin.vent.trigger("grid:pager:next");
},
last: function(event) {
Admin.vent.trigger("grid:pager:last");
}
});
// The grid layout with the regions to display the different part of the data grid
Layout = Backbone.Marionette.Layout.extend({
template: "layouts/grid",
regions: {
gridTable: "#gridTable",
gridQuickSearch: ".gridQuickSearch",
gridItemPageSelector: ".gridItemPageSelector",
gridPager: ".gridPager"
}
});
// Once the layout is rendered, the different views are attached to the right regions
Admin.vent.on("grid:layout:rendered", function() {
var itemPageSelectorView, pagerView, quickSearchView;
quickSearchView = new QuickSearchView();
Admin.gridLayout.gridQuickSearch.attachView(quickSearchView);
itemPageSelectorView = new ItemPageSelectorView();
Admin.gridLayout.gridItemPageSelector.attachView(itemPageSelectorView);
pagerView = new PagerView();
Admin.gridLayout.gridPager.attachView(pagerView);
});
// Initializer to do at the application start
GridLayout.addInitializer(function() {
Admin.addRegions({
content: ".content-box"
});
Admin.gridLayout = new Layout();
// Trigger the rendered event when the grid layout is shown
Admin.gridLayout.on("show", function() {
Admin.vent.trigger("grid:layout:rendered");
});
// Manage the collection data change
Admin.vent.on("grid:collection:change", function(collectionName) {
// Close the previous view in the grid table region
Admin.gridLayout.gridTable.close();
// Trigger an event to fetch the collection
Admin.vent.trigger("" + collectionName + ":collection:fetch");
// Show the grid layout if not already done
if (!this.shown) {
this.shown = true;
Admin.content.show(Admin.gridLayout);
}
});
});
return GridLayout;
});
We are done on the structural code. Now we can go to one of the controller. For example, the ThemeController:
MyApp.Admin.module("ThemeController", function(ThemeController, Admin, Backbone, Marionette, $, _) {
// Define the model to use in the collection
ThemeController.Theme = Backbone.Model.extend();
// Define the collection with the related url on the server. The collection extends a paginated collection that has the methods to manage the quick search and the pagination
ThemeController.Themes = Backbone.ExtendedCollection.paginatedCollection.extend({
url: "/admin/themes",
model: ThemeController.Theme,
initialize: function() {
var _this = this;
//
// The following functions handle the events for the quick search and pagination
//
Admin.vent.on("grid:quickSearch:term", function(term) {
_this.quickSearch(term);
});
Admin.vent.on("grid:itemPageSelector:count", function(count) {
_this.perPage(count);
});
Admin.vent.on("grid:pager:previous", function() {
_this.previous();
});
Admin.vent.on("grid:pager:next", function() {
_this.next();
});
Admin.vent.on("grid:pager:first", function() {
_this.first();
});
return MyApp.Admin.vent.on("grid:collection:fetched", function() {
Admin.gridLayout.gridTable.show(new Admin.ThemeView.Table({
collection: _this
}));
});
}
});
// At the application initilization, we need to be sure this controller can
// handle the event to fetch the data from the server
Admin.addInitializer(function() {
Admin.vent.on("themes:collection:fetch", function() {
ThemeController.themes = new ThemeController.Themes();
// Once the data are fetched from the server, trigger an event to display them
ThemeController.themes.fetch({
success: function() {
Admin.vent.trigger("grid:collection:fetched");
}
});
});
});
});
And finally the views for the previous controller:
MyApp.Admin.module("ThemeView", function(ThemeView, Admin, Backbone, Marionette, $, _) {
// The view to show one item in a row of the data grid
ThemeView.Item = Backbone.Marionette.ItemView.extend({
tagName: "tr",
template: "admin/themes/theme",
model: Admin.ThemeController.Theme
});
// The view to show the collection of item
ThemeView.Table = Backbone.Marionette.CompositeView.extend({
template: "admin/themes/index",
model: Admin.ThemeController.Theme,
itemView: ThemeView.Item,
collection: Admin.ThemeController.Themes,
itemViewContainer: "#themes",
// ! I was force to add this to have data in the original format that is used by my templates !
serializeData: function() {
return this.data;
}
});
});
Remark: The subtheme controller and view files contains exactly the same kind of codes. Only templates and kind of stuffs differ.
The grid layout in HAML compiled through Rails assets pipeline looks like:
.gridPager
%button#first= "<<"
%button#previous= "<"
%button#next= ">"
%button#last= ">>"
%span.gridItemPageSelector= "Item per page"
%select
%option= 5
%option= 10
%option{"value" => -1}= "All"
%span.gridQuickSearch= "Quick search:"
%input#gridSearchTerm{"type" => "text"}
#gridTable
%span.gridItemPageSelector= "Item per page"
%select
%option= 5
%option= 10
%option{"value" => -1}= "All"
%span.gridQuickSearch= "Quick search:"
%input#gridSearchTerm{"type" => "text"}
.gridPager
%button#first= "<<"
%button#previous= "<"
%button#next= ">"
%button#last= ">>"
As you can see, there is quite a lot of repetition. I wanted to have quick search and pagination on top and bottom of my grid. At the moment, the simplest way to do that is to duplicate the code. I will change that later when I will find how to do that.
The template for table that shows the themes:
%table.table.table-striped
%thead
%tr
%th= "Id"
%th= "Name"
%tbody#themes
Quite simple and nothing special to say. At this time, the headers are hardcoded !
And finally, the item view template to show a theme:
%td= this.id
%td= this.name
This template is realy simple.
I am in a situation were that is pretty well working. For example, when I click on other links to change the collection shown, the quick search fields and kind stuff like that are not reinitialized. For that, I wanted to add a sort of state management to keep trace of the collection state and when come back to an already shown collection, I want to show it as it was previously.
I am sure that my solution is not perfect and could be refactored a lot. I also probably did a lot of "newbie" mistakes. So feel free to challenge my proposition. I try to learn and improve my solution and hope it will help someone to do something like that.
Well I'm not a big expert but that's how I did it, using Marionette and Requirejs:
a) I created a generic Grid Layout wiew that is called by my approuter with some parameter like collection, cols config (I render the table head with a each cycle) and a row view:
showUsers: function(){
require(['views/GridGen','collections/user_collection'], function(Grid, UserCollection){
var Users = new UserCollection();
App.grid = new Grid({collection: Users ,
rowView: 'rowUser',
cols_config: App.tables.users});
App.page.show(App.grid);
});
},
b) in my Grid Layout I render the various pieces waiting for the onShow event:
var Grid = Backbone.Marionette.Layout.extend({
regions: {
top_controls: "#top_controls",
table_view: "#table_view",
pagination_controls: "#pagination_controls",
bottom_controls: "#bottom_controls",
},
onShow: function(){
this.renderTable(this.collection);
},
renderTable: function(collection){
collection.fetch({success:function(){
require(['views/'+self.options.rowView+'.js'],function(iView){
var vista = new View({collection: collection, itemView: iView, thead: self.options.cols_config});
App.grid.table_view.show(vista);
self.renderPagination(collection);
collection.pager();
});
}});
}
c) my generic Table View take the cols and the itemView to render as parameter like this:
var View = Backbone.Marionette.CompositeView.extend({
initialize: function(){
this.itemView = this.options.itemView;
},
serializeData: function(){
var data = Backbone.Marionette.ItemView.prototype.serializeData.apply(this, arguments);
data.thead = this.options.thead;
return data;
},
appendHtml: function(collectionView, itemView, index){
collectionView.$("tbody").append(itemView.el);
},
That's just a general idea, I don't think it's the best way to do it but I didn't find a better solution yet, hope to give you at least some hints :)
I highly recommend the backgrid component which is extensible and reusable out of the box.
Only at version 0.2.6 - but good following and its pretty slick
I'm trying to add a bunch of components to my container using data populated in my store. I want to iterate through my stores records and output a label/panel/etc to display the data.
This is how I'm looping through now:
initComponent: function () {
//Create a container for each array, then in that container,
//create a label and wrapping panel, then add the items to the wrapping panel
this.store = Ext.create('Ext.data.Store', {
model: 'MyProject.model.ItemArray',
data: []
});
Ext.apply(this, {
items: [
this.store.each(function() {
{
xtype:'label', text:
'test'
}
})
]
});
this.callParent(arguments);
}
But of course, being the EXTJS noob that I am, this doesn't work. I would appreciate any advice and best practice tips you can provide to get this working.
My model, ItemArray, contains Items. I need to loop through my store of ItemArrays and make containers, then loop through the Items in the ItemArray populate those containers with Items.
Thanks everyone!
You need to load your store first, then on the store callback generate your labels.
var me = this;
me.store.load(function(records) {
Ext.each(records, function(record) {
//Add a container for each record
me.add({
xtype: 'container',
items: [] //your labels here
});
});
});
Your store is not populated yet in your example. It also loads asynchronous so there will be a slight delay.
Ext.define('Foo', {
initComponent: function(){
// assumes store is loaded with data
var store = new Ext.data.Store(),
items = [];
store.each(function(rec){
items.push({
html: rec.get('aField')
});
});
this.items = items;
this.callParent();
}
});
I'm trying to create a grid inside a grid row when I expand the row using the rowexpander plugin. How do I render the subgrid on the expandbody event?
This is my code so far, it is used as an event handler property when I define my grid panel:
getOtherProducts: function (rowNode, record, expandRow, eOpts) {
$.ajax({
type: 'GET',
url: "Report.aspx/GetOtherProducts",
data: { ID: record.data['ID'] },
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function () {
var subStore = Ext.create('pr.store.Store-Products');
subStore.proxy.extraParams.productID = record.data['ID'];
var subGrid = Ext.create('Ext.grid.Panel', {
store: subStore
});
subGrid.getEl().swallowEvent(['mouseover', 'mousedown', 'click', 'dblclick']);
},
error: function () {
showNotificationBar("Error retrieving product data. Re-expand the row to try again.");
}
});
},
stratboogie's answer at http://www.sencha.com/forum/showthread.php?151442-Nested-EXTJS-4-Grids did the job.
I made a slight modification to store the Element ID into an array
subGrids.push(subGrid.id);
and then overrode paging event handlers to loop through the array and destroy all elements with the IDs inside the array to keep memory in check.
function destroySubGrids(){
Ext.each(subGrids, function(id){
var subGrid = Ext.getCmp(id);
if(subGrid){
subGrid.destroy();
delete subGrid;
}
});
subGrids = [];
console.log(Ext.ComponentMgr.all.length); //debug
}