PDA

View Full Version : cascading close



walldorff
9 Nov 2011, 2:46 AM
I'm building an application with nested components, where each component has its own (one or more) help windows and I'm struggling with the application design for those windows.
I would like to close the help windows (when open of course) by the component which owns them, when the component is closed.
Here's some code to make it more clear. Ext version = 3.4.


var form = new Ext.TabPanel({
activeTab : 0
// more configs
// items layout = form
,items : [{
title: 'Personal Info'
,help: new Ext.Window({/* f1 */})
,items: [/* items */]
},{
title: 'Business Info'
,help: new Ext.Window({/* f2 */})
,items: [/* items */]
}]
});

var grid = new Ext.grid.GridPanel({
// form goes here
plugins: form
,help: new Ext.Window({/* g1 */})
});

var tabs = Ext.TabPanel({
activeTab : 0
// more configs
,items : [{
title: 'Contacts Grid'
,help: new Ext.Window({/* t1 */})
// grid goes here
,items: grid
},{
title: 'Branches Grid'
,help: new Ext.Window({/*t2 */})
,items: [/* items */]
}]
});

var win = new Ext.Window({
title: 'Main Window'
// more configs
,help: new Ext.Window({/* w1 */})
// tabs goes here
, items: tabs
});

So, if form gets closed, the windows f1 and f2 have to be closed too, while win.onclose must close all the help windows which are descending from it.

I was thinking in the line of extending each component with a winArr variable, in which the Ext generated id's can be stored. At onClose winArr can be traversed in order to close all the owned help windows. But how to get to the right child component(s), without Ext.getCmp() ?
Of course there is a better way to do this. I just need a kick in the right direction.

mitchellsimoens
9 Nov 2011, 4:40 AM
I think this is a great case for an app extension to be made...

Have the class extend Ext.Window which would build the TabPanel and put the help window on each tab. Now this class will handle everything for you, have a close listener that will traverse the items of the TabPanel to make sure all help windows are closed.

walldorff
9 Nov 2011, 5:30 AM
Thanks Mitchell.

I think this is a great case for an app extension to be made....
Figured that :)

Extending Ext.Window with build facilities is a great idea and doable. I think Saki wrote something about this.
Should I proceed with property winArr as a placeholder for the id's? Or is there another way to let the parent know which help windows to close?

walldorff
9 Nov 2011, 9:46 AM
I've found a lazy way out. I like to know if this is also a lousy way out.

All my Help-windows are hidden on close (closeAction='hide'). Instead of figuring out how to cascade-close them, I close them in batch.
This will be taken care of by a handler class:


/**
* @class HelpWin
* @desc Class to handle Ext.Window objects.
* Upon initialize a unique id is generated and glued to the prefix.
* The prefix is mandatory and serves as a group name.
* Windows can be closed (hidden) in batch, based on the prefix.
* Some code was nicked from the Ext class :)
* @singleton
*/

HelpWin = {};

(function(){
// private static vars
var idSeed = 0;
var winArr = [];
var window = Ext.extend(Ext.Window,{
closeAction: 'hide' // allways hide. we never close the window.
,visible : false // visibility: none, so we can show a window at convenience.
,width : 400
,height : 400
,title : 'Help'
,iconCls : 'help'
,modal : false
,autoScroll : true
,collapsible : true
,constrain : true
,bodyStyle : 'padding:10px;'
,listeners : {
afterrender : function(self) {
self.hide()
}
}
});

Ext.apply(HelpWin, {

// generates an id and pushes it on the winArr stack
push : function(prefix) {
var id = prefix + '-' + (++idSeed);
winArr.push(idSeed);
return id;
}

// makes the help-window with the config
,make : function(prefix, config) {
if (!prefix) {
alert('prefix missing on make');
} else {
config.id = this.push(prefix);
var win = new window(config);
return win;
}
}

// hides the windows with the given prefix
,close : function(prefix) {
if (!prefix) {
alert('prefix missing on close/hide');
} else {
for (var i=1;i<=idSeed;i++){
var id = prefix + '-' + i;
var win = Ext.getCmp(id);
if (win && win.isVisible()) {
win.hide();
}
};
}
}
// conveniency function, calls close()
,hide : function(prefix) {
this.close(prefix);
}
});
})();

Help can be popped up from a button:


// the button
helpBtn = new Ext.Button({
text: 'Help'
// more configs
,handler : this.onHelpClick
});

// the method
onHelpClick: function (btn) {
// there's only one instance of this window.
// the prefix is simply a group name as string ('mygrid', or 'myform'),
// which HelpWin uses to identify a group to batch-close all open windows
arguments.callee.win = arguments.callee.win || HelpWin.make(this.getPrefix(), {
title : 'Help on this subject'
,autoLoad : {url: 'path/to/help.php', scripts:true}
,layout: 'fit'
,listeners: {hide: function(){btn.enable();}}
});
btn.disable();
arguments.callee.win.show();
}

Now comes the tricky part. When I have a grid inside a tab panel, which is inside a window, then I have to get to the correct component, in order to close all help windows.
Closing a tab would require something like this:


pnl.on('beforeremove',function(cont,comp) {
var items = comp.initialConfig.items; // ?? hmm...
HelpWin.close(items.getPrefix());
},this);

Closing the window looks a bit messy.


win.on('beforeclose',function(comp) {
var items = comp.initialConfig.items.initialConfig.items; // boohoo!
if (!Ext.isArray(items)) {
items = [items];
}
Ext.each(items, function(item) {
if(item.items) {
HelpWin.close(item.items.getPrefix());
}
})
},this);

I am not happy with the 'initialConfig.items' and 'item.items' way of coding. I think it's confusing and I am sure that I miss something here. Is there a more proper way to achieve this? Thanks!

walldorff
11 Nov 2011, 7:55 AM
Not satisfied with the so called lazy option, I finally took Mitchell's advice (sorry Mitchell :">) and did some proper extending.

The component, which holds & constructs the help windows has an array as a placeholder for the id's wins: []
On button click a method looks for the id of the button in wins. If found: win.show().
If not, then new Ext.Window({btn: btn, ...}) and push an object with the id's of the button and window onto the wins stack.


onHelpClick: function (btn) {
var index = this.wins.hasId(btn.id);
var win;
if(index < 0) {
win = new Ext.ux.HelpWin({
title : 'Help about this matter'
,autoLoad : {url: 'path/to/help.php', scripts:true}
,btn: btn
,listeners : {
afterrender : function(self) {
self.hide()
}
,hide: function(self) {
self.btn.enable();
}
}
});
this.wins.push({btn:btn.id, win:win.id});
} else {
var winId = this.wins[index].win;
win = Ext.getCmp(winId);
}
btn.disable();
win.show();
}



closeWins: function() {
// destroy all helpwindows
Ext.each(this.wins, function(o) {
var win = Ext.getCmp(o.win);
win.destroy();
});
this.wins = [];
}

On close of the parent component closeWins() can be called.

darthwes
11 Nov 2011, 4:15 PM
OK, I really like to give code instead of comments, as we should all be able to read code, but I needs m0ar code, as I can't flesh out what you're doing entirely just from the code I see here. But allow me to make some observations:

You (probably) don't wanna do



Some.Other.Object = Ext.extend(Library.Basic.Object, {
..,
anyvar: [],
..
});


You're assigning a single array for the prototype, all instantiations now use the same array unless you define it, again, on the object. I know it's hard to read that sentence, try it a few times. Basically, it's not what you really want.

That's why sometimes in the code you'll see a big comment block with no definition of the actual attribute in the config object passed to extend (which get applied onto the prototype of the object that will be used as the prototype for your instances). Use initComponent to enforce proper behavior by doing a



Some.Other.Object = Ext.extend(Library.Basic.Object, {
..,
initComponent: function () {
this.anyvar = [];
},
..
});


Right, so don't keep the ids, just don't. You have a reference to the object and yet you store the id? You coulda went with just keeping the reference (the variable that you used) instead of its id. So don't do that.

I see you've used TabPanel but you didn't use the new keyword, that's not right, use the new keyword...You'll screw the prototype chain (and scope).

But I don't even get why you're using a TabPanel at all?

I don't really understand what you're doing. I think you're spawning a buncha windows so the user can have multiple help windows up at the same time? In which case, you're on that particular path, I suppose. I'd say that you're thinking all wrong, the best solution is event-driven: respond to your parent window closing by self closing. And, of course, remove that listener if the user closes that help window. So you don't really need a reference to the windows at all. It's a subclass, custom component, or whatever nomenclature you prefer. I'll leave the exercise to the reader :) Hope that helped some.

darthwes
11 Nov 2011, 4:16 PM
I just auto-tldr'ed...

walldorff
11 Nov 2011, 7:47 PM
Thanks Wes. The code in my first post was sort of pseudo-code, I corrected it.


I can't flesh out what you're doingGlad to give you more details. I am modifying on Todd Murdock's fantastic qWikiOffice (http://qwikioffice.com/) (Sencha post (http://www.sencha.com/forum/showthread.php?10950)). In short: qWikiOffice has modules, which have dataViews and grids. A dataView serves as menu to open tabs with grids.
A module is an Ext.Window which has an Ext.TabPanel inside. The first tab is always a dataView with icons as a menu. Each one opens/shows a tab, which contains an Ext.EditorGridPanel. To add/edit a record the grid presents a modal Ext.Window, which contains another Ext.TabPanel. Each of those tabs are part of the form.
I want to enable the user to open a help window on each grid and on each tab of the form. So the hierarchy, graphical and a bit simplyfied:

desktop
|
+--window 1 (module)
| |
| +--tabpanel 1
| |
| +--tab 1.1: dataview
| |
| +--tab 1.2 (closable): gridpanel
| | |
| | +--window 1.1 (help)
| | |
| | +--window 1.2 (modal)
| | |
| | +--tabpanel 2 (form)
| | |
| | +--tab 2.1: personal info
| | | |
| | | +--window 2.1 (help)
| | |
| | +--tab 2.2: professional info
| | | |
| | | +--window 2.2 (help)
| | |
| | +--tab 2.3: other info
| | |
| | +--window 2.3 (help)
| |
| +--tab 1.3: gridpanel
| |
| etc
|
+--window 2 (module)
| |
| etc

Closing windows is hiding them, unless the module gets closed; then they are destroyed.
Now, regarding the closing of the help windows, the app has to respond as follows:


event
action


closing the form (window 1.2)
A: close help windows 2.1, 2.2 and 2.3


closing the grid (tab 1.2)
B: close form + action A + close help window 1.1


closing the module
action B + action A


In order to close the right windows I thought I should store references of associated button+window in pairs as an object in an array. Component must know which helpwindow already exists, so I bind it to the clicked helpbutton, because that is already rendered in the comp. If - on button click - the btn ref is found, then the helpwin exists, so show it. If not, push the refs and show a new window.
single array for the prototype, all instantiations now use the same array unless you define it, again, on the object. [...]
Basically, it's not what you really want.That's correct.

I'm indeed a bit stuck on how to do this properly, so your observation of my wrong way of thinking could be absolutely accurate ;).

the best solution is event-driven: respond to your parent window closing by self closing. And, of course, remove that listener if the user closes that help window. So you don't really need a reference to the windows at all.
I'm going to study on what you gave me. Thank you for that! And sorry for the TL.

Roland

darthwes
13 Nov 2011, 10:09 AM
closing the form (window 1.2)
A: close help windows 2.1, 2.2 and 2.3






Closing windows is hiding them, unless the module gets closed; then they are destroyed.


So, A doesn't really have an action. We have a window with closeAction: 'hide', which has a tabpanel which has some tabs. When the user closes the window, it's hidden, so all of its children can remain, as that is the process (hide not destroy). Nothing special yet.





closing the grid (tab 1.2)
B: close form + action A + close help window 1.1








My first question is what events happen when we close this grid, and also I assume we're really hiding the grid, not closing it? In which case I'd probably just have all the windows I made from that grid panel respond to grid 'hide' event by hiding themselves and the grid 'show' event by showing themselves. I'd install those listeners at instantiation time to the grid, perhaps creating an extension on Window to allow me to give the grid object to the window so the window can self-install the listeners.





closing the module
action B + action A








This is not what you originally said (you said if the module was closed, we destroyed the components, not hide them). To perform what you originally said (destroy them), make the first-level window closeAction: 'close' and then all I would do is something like:



My.Special.Window = Ext.extend(Ext.Window, {
closeAction: 'hide',
logicBindToParent: function (parent) {
parent.on('hide', this.hide, this);
parent.on('show', this.show, this);
parent.on('beforedestroy', this.close, this);
}
});


Which would do all the work for me, and I've solved the problem in 7 lines. Note that Ext will purge listeners for me so I don't need to worry about uninstalling them (window close chains into window destroy by default).

Hope that helps.

walldorff
13 Nov 2011, 4:16 PM
Wes, you saved the day! Your 7 lines did the trick. My coin took a while to drop. Life can really be simple if you look in the right vicinity.

My first question is what events happen when we close this grid, and also I assume we're really hiding the grid, not closing it?
No, the grid is inside a tab and and destroyed @ onClose. And with your solution, so are the help windows.

This is not what you originally said (you said if the module was closed, we destroyed the components, not hide them)
I'm sorry, you're right. In the diagram this had to be proposed as action C, so to speak.

I will implement this throughout the application.
Thank you for the enlightening and great help!

Roland