PDA

View Full Version : Nested gridpanels: row focus and select



htoyryla
9 Nov 2009, 2:06 AM
I have a user requirement for a nested grid, i.e. some grid cells need to contain another grid. I have seen some comments that tend to discourage placing complex stuff inside a grid cell, but here I am with this requirement.

I have done some progress with the implementation, but there is a problem: row focus and selection within an inner grid also affects the row with the same index in the outer grid. For instance, if mouse is on row 1 of the outer grid, but over row 3 in an inner grid, both rows 1 and 3 get focus outlook in the outer grid.

I gather I need to do something to stop the inner grid events from affecting the outer grid, so I have looked for corresponding events in the documentation but cannot find any.

Edit: The core of the problem appears to me to be that the rowIndices of the two grids are not distinguished from each other.

htoyryla
9 Nov 2009, 3:34 AM
Here's an image showing roughly what I am trying to achieve: http://freygish.com/hannu2/nestedgrid.jpg

Edit: I guess speaking of row focus is incorrect, I mean the highlighting of the row when mouse is over the row.
I can hide this symptom of the problem from the user through css, I guess, although evntually i hope to be able to handle the row highlights also properly (which in my case means highlighting the correct rows in both grids).

htoyryla
9 Nov 2009, 6:10 AM
After digging more into this issue, I start to suspect that the gridpnael is fundamentally incapable of containing another gridpanel. For instance, clicking on a cell for editing can affect a completely wrong cell. Another minor issue was that after editing a cell (in the outer grid), the whole row is re-rendered so that the inner grids need to be recreated.

I think will give up this track and try to come up with an alternative solution.

jsakalos
9 Nov 2009, 7:53 AM
I start to suspect that the gridpnael is fundamentally incapable of containing another gridpanel Yes, that is true. GridPanel's view removes all inner html on refresh so no ext component should be within the grid. Sure, you could code a special handling to allow that, however, it wouldn't work OOTB.

htoyryla
9 Nov 2009, 8:23 AM
Yes, that is true. GridPanel's view removes all inner html on refresh so no ext component should be within the grid. Sure, you could code a special handling to allow that, however, it wouldn't work OOTB.

I was prepared to do special handling for this issue, though. The bigger problem was that the rows and columns of the inner grid get mixed up with the outer grid and vice versa.

jsakalos
9 Nov 2009, 8:31 AM
I see. If the outer grid is simple enough, you could a data view or table layout instead of grid. Of course, you'd lose headers, sorting, etc... I don't know if that would be the way for you, just an idea...

htoyryla
9 Nov 2009, 8:38 AM
I see. If the outer grid is simple enough, you could a data view or table layout instead of grid. Of course, you'd lose headers, sorting, etc... I don't know if that would be the way for you, just an idea...

I'm going to do it the other way round. What would have gone into an inner grid will just be html content inside grid cells. For editing of this data, a popup with a real inner grid will be opened. Not exactly as specified but, given the time schedules, acceptable.

jsakalos
11 Nov 2009, 6:02 AM
Yes, that should work.

htoyryla
30 Jan 2010, 1:51 AM
I am raising this issue again because the alternative solutions have turned out to be too clumsy from the users point of view.

It seems to me that the main problem with having a grid nested inside another is that events from the inner grid also affect the outer grid (rows in the outer grid get focus and selected when they shouldn't). I have seen somewhere an override to grid.processEvent to prevent this, but couldn't get it to work. Any ideas how to do that in 3.1 ? I.e. to keep the events within the grid from which they originate.

This is what I tried:
http://www.targetprocess.com/agileproductblog/2008/10/targetprocess-development-tricks-force.html
Of course, the jQuery part needs to be changed. But it appears that when mouse is moved to give a row focus, grid.processEvent is not called (it is called when a row is clicked).

Addenda: Using the code below I can now stop the processing of events in the outer grid, if they originate from the inner grid, using an override based on the code from the link above. But moving the mouse in the inner grid still highlights rows also in the outer grid, as these events do not cause grid.processEvent to be called. So where do they go?



Ext.override(Ext.grid.GridPanel, {
processEvent: function(name, e) {
var t = e.getTarget();

if (!t) {
return;
}

if (!this.el) {
return;
}

function isInner(el) {
// find out if el is in an inner grid
var par1 = Ext.get(t).findParent('.x-grid3') ;
if (!par1) return true ; //
var par2 = Ext.get(par1).findParentNode('.x-grid3') ;
return (par2) ;
}

// find out number of grid child nodes
var grid_children = Ext.get(this.el.id).query('.x-grid3') ;
// stop if this has inner grid children and the event originates from an inner grid
if ((grid_children.length > 1) && (isInner(t))) return ;

//... continue with event processing

jsakalos
30 Jan 2010, 3:31 AM
The events could be probably stopped to not reach the outer grid. The main problem, IMO, is the way how the grid updates its view (html markup), as I write above. It first unconditionally removes the markup and then creates new. Imagine you have an inner grid rendered in a cell of the outer grid. Now you refresh the outer grid. Html markup of the inner grid is removed, if you don't perform some actions to preserve it, but the javascript object, including all listeners, is still in the memory.

htoyryla
30 Jan 2010, 4:39 AM
I understand that there is memory management problem related to the refresh.

Still, we are facing a recurring requirement pattern here. There is a grid where rows represent data such as


goal:{id: 1, title: "something", value: 123, actions: [{id: 123, title: "asb", owner:{id: 123, name: "John Smith}},....], status: 'ok'}
and the most natural way to present the actions would be to place an inner grid into a cell.

For purely display purposes I can manage with a single grid having rows for action_title and action_owner and using a custom renderer. The problem is that for editing, I need to open a popup with a grid using goal.actions as the data, but this is very weak and counterintuitive from usability point of view.

BTW, I think I've done nested grids with YUI but am not 100 % sure. But I remember having written custom renderers for YUI datatables which examined and modified the existing markup in the cell.

htoyryla
30 Jan 2010, 5:00 AM
Now you refresh the outer grid. Html markup of the inner grid is removed, if you don't perform some actions to preserve it, but the javascript object, including all listeners, is still in the memory.

In my nested grid experiment, the custom renderer works roughly like this:

- create an empty div
- create the inner grid component if it does not yet exist
- return the empty div
- using 'afterrender' event plus some delay, render the inner grid into the cell

It is not fully working yet, there are at least sizing problems, and it is probably wasteful and inefficient. I am not sure if this path is worth pursuing, but I needed to revisit this issue because the UI requirement has resurfaced.

UPDATE: Found out that this approach fails because once a nested grid has been rendered, it is internally marked as rendered and cannot be recreated by calling its render() function. Anyhow, such experiments at least increase my understanding on how ExtJS works.

htoyryla
30 Jan 2010, 10:56 AM
I have made two experiments on nested grids:
1 storing and restoring html
2 storing and re-rendering the nested grids

The first approach proved difficult, while the second worked, although the solution obviously is not a clean one. I will present it anyway.

Here's the example itself. All fields are editable. Inner grids are rendered using a delay so they do not appear immediately.



<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>Nested Grid Experiment using re-rendering</title>
<link href="ext/resources/css/ext-all.css?1246921761 (http://www.extjs.com/forum/view-source:http://freygish.com/ext/resources/css/ext-all.css?1246921761)" media="screen" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="ext/adapter/ext/ext-base.js (http://www.extjs.com/forum/view-source:http://freygish.com/ext/adapter/ext/ext-base.js)"></script>
<script type="text/javascript" src="ext/ext-all-debug.js (http://www.extjs.com/forum/view-source:http://freygish.com/ext/ext-all-debug.js)"></script>
<script type="text/javascript" src="extest/gridNestingOverride.js (http://www.extjs.com/forum/view-source:http://freygish.com/extest/gridNestingOverride.js)"></script>
<script>

Ext.BLANK_IMAGE_URL = 'ext/resources/images/default/s.gif';

// array for storing inner grids for later reference
var innerGrids = [] ;
var grid = null ;

Ext.onReady(function() {

// sample static data for the main grid store
var myData = [
[1,'The Home Depot, Inc.',34.64,0.35,1.02,'9/1 12:00am'],
[2,'The Procter & Gamble Company',61.91,0.01,0.02,'9/1 12:00am'],
[3,'United Technologies Corporation',63.26,0.55,0.88,'9/1 12:00am'],
[4,'Verizon Communications',35.57,0.39,1.11,'9/1 12:00am'],
[5,'Wal-Mart Stores, Inc.',45.45,0.73,1.63,'9/1 12:00am']
];

// sample data to initialize inner grids
var myData2 = [['AAA', 123], ['BBB', 456], ['CCC', 789]] ;


// create the main grid data store
var store = new Ext.data.ArrayStore({
fields: [
{name: 'id'},
{name: 'company'},
{name: 'price', type: 'float'},
]
});

// manually load local data
store.loadData(myData);


// function to create a new inner grid when needed
function newGrid(rec) {
// create store instance
var store2 = new Ext.data.ArrayStore({
autoSave: true,
fields: [
{name: 'product'},
{name: 'code'}
]
});

// initialize with sample data
store2.loadData(myData2) ;

// create grid
var g = new Ext.grid.EditorGridPanel({
store: store2,
trackMouseOver: false,
header: false,
autoHeight: true,

columns: [
{header: 'Product', width: 100, dataIndex: 'product', editor: new Ext.form.TextField()},
{header: 'Code', width: 75, dataIndex: 'code', editor: new Ext.form.NumberField()}
]
}) ;
return g ;
}


// custom renderer for creating and recreating a inner grid
function gridRenderer(value, metaData, record, rowIndex, colIndex, store) {
var v = rowIndex ;
console.log("rendering inner grid on row "+v) ;
var html = "<div id='nestedgrid-"+v+"'></div>" ; // placeholder div for the cell
var g = null ;
if (innerGrids[v] !== undefined) { // the grid already exists
g = innerGrids[v] ; // so get it
console.log("using existing grid "+v) ;
}
else {
g = newGrid(record) ; // the grid is needed first time, so create it
innerGrids[v] = g ; // and store for later reference
console.log("creating new grid "+v) ;
}

var renderNestedGrid = new Ext.util.DelayedTask( // set a delayed task to render the grid into the cell
function () {
g.rendered = false ; // set rendered to false to achieve re-rendering
g.tools = null ; // set tools to null, otherwise an empty object may cause an error on rendering
g.render('nestedgrid-'+v) ; // and render the inner grid in the cell
console.log("...to nestedgrid-"+v+" now!") ;
}) ;

renderNestedGrid.delay(1000) ; // todo: use some approriate event here if possible

return html ; // return the placeholder div
}

// create the main Grid
grid = new Ext.grid.EditorGridPanel({
store: store,
trackMouseOver: false,
autoHeight: true,
columns: [
{id:'company',header: 'Company', width: 160, sortable: true, dataIndex: 'company', editor: new Ext.form.TextField()},
{header: 'Price', width: 75, sortable: true, renderer: 'usMoney', dataIndex: 'price', editor: new Ext.form.NumberField()},
{header: 'Test', width: 175, sortable: true, renderer: gridRenderer, dataIndex: 'id'}
],
stripeRows: true,
autoExpandColumn: 'company',
height: 350,
width: 600,
title: 'Nested Grid Example',
});


var mainlayout = new Ext.Viewport({
layout: 'fit',
layoutConfig: { scrollOffset: 19 },
autoScroll: true,
items: [
grid
]
}) ;

}) ;

</script>

</head>
<body>
</body>
</html>
And here's the override to isolate events between the inner and outer grids. This based on code from the link I quoted above.




Ext.override(Ext.grid.GridPanel, {
processEvent: function(name, e) {
var t = e.getTarget();

if (!t) {
return;
}

if (!this.el) {
return;
}

function isInner(el) {
// find out if el is in an inner grid
var par1 = Ext.get(t).findParent('.x-grid3') ;
if (!par1) return true ; //
var par2 = Ext.get(par1).findParentNode('.x-grid3') ;
return (par2) ;
}

// find out number of grid child nodes
var grid_children = Ext.get(this.el.id).query('.x-grid3') ; //.length > 1 && Ext.get(t).parent('.x-grid3')) return ;
// stop if this has inner grid children and the event originates from an inner grid
if ((grid_children.length > 1) && (isInner(t))) return ;

// continue with event processing
// todo, this code needs revising, is probably from Ext2 times
this.fireEvent(name, e);

var v = this.view;
var header = v.findHeaderIndex(t);

if (header !== false) {
this.fireEvent("header" + name, this, header, e);
} else {
var row = v.findRowIndex(t);
var cell = v.findCellIndex(t);
if (row !== false) {
this.fireEvent("row" + name, this, row, e);
if (cell !== false) {
this.fireEvent("cell" + name, this, row, cell, e);
}
}
}
}
});
I am not saying this is something I am planning to use. Just groping in the dark. Still, if this kind of UI requirement were taken seriously, it should not be too difficult for ExtJS to make it a bit easier.

UI-wise, this ( http://freygish.com/extest3.html ) is still not an ideal solution. Having separate column headings in each inner grid should be avoided. Instead, it would be better to have the corresponding headings within the column heading in the main grid, so that the user does not really see two levels of grids but a single, hierarchically organized grid.

jsakalos
30 Jan 2010, 4:24 PM
What you could do: before grid view removes the html, move inner grid.el (all of them) to the body. You can hide 'em en route. Then let grid view to re-render the grid and after that moved grid.els back to their place, or destroy them, or create new, whatever.

htoyryla
31 Jan 2010, 9:21 AM
What you could do: before grid view removes the html, move inner grid.el (all of them) to the body. You can hide 'em en route. Then let grid view to re-render the grid and after that move grid.els back to their place, or destroy them, or create new, whatever.

Sounds like a clear approach. If I understand it correctly, this means a customized GridView. I'll see if I can find the time to try this.

jsakalos
31 Jan 2010, 2:03 PM
Also, it works. I've tried it for custom grid headers containing form fields and the way I describe above is proven to work. If it works for headers, there is no reason why it shouldn't work for body, rows.