PDA

View Full Version : setting up correct layout code



albeva
2 Jun 2007, 4:03 AM
I am now developing an application based on ext. and have come with rather dirty and hackish code for creating my layout that doesn't always seem to work well (especially resizing and panel collapsing) so I was hoping that someone here might give me a hand on how to create such layout using only js - dynamically generate all content. Basic layout of my application is:
----------------------
toolbar
----------------------
T |
R | tabbed content
E | with optional
E | toolbar
----------------------
footerwhere tree can be reloaded to show different info. and in tabbed area I have different content in tabs - in one TinyMCE editor, or Ext.Grid or Ext.Form. IE7 for example doesn't see to like forms inside a tab. I got that hackishly solved with setting "position: relative"

in tabs I have optional toolbars. With grids it's simple - I just get headerPanel and it works. fot forms I have to setup inner BorderLayout...

My general way of doing this is: I create a BorderLayout and set west, north and south to toolbar, tree and footer and leave center for tabbed content. now inside tabs if I have a grid - it doesn't seem to really resize grid when I collapse the tree panel. and with forms it's even worse - the page doesn't resize at all.


/**
* Layout handler
*
* The layout has a tree control on the left
* side and tabbed content on the right side.
*
* Also register the module properly with Ant
* subsystem and such.
*
* @author Albert Varaksin
*/

/**
* Create a base object for inheriting
*/
var AntTreeTabbedContent = function () {};
var p = AntTreeTabbedContent.prototype;


/**
*
* @param Object config = {
* tree : Config
* treeRoot : Config
* treeTitle : string
* }
*/
p.fnLoadLayout = function (config)
{
// set close tab event
cr = Ant.layout.getRegion('center');
cr.on ('panelremoved', this._fnOnPanelRemoved, this);

// Create tree control
treePanel = new Ext.ContentPanel(Ext.id(), {autoCreate:true, title:config.treeTitle, fitToFrame:true})
this._treePanel = treePanel;

var Tree = Ext.tree;
this._tree = new Tree.TreePanel(treePanel.getId(), config.tree);
this._tree.setRootNode(new Tree.AsyncTreeNode(config.treeRoot));
this._tree.render();
this._tree.root.expand();

// Look for node renaming
this._tree.on ('textchange', function (node) {
var m = Ant.fnGetModuleByTreeId(node.id)
if (m) m.panel.setTitle (node.text);
}, this);

// Look for node removal
var self = this;
function fn (node)
{
var m = Ant.fnGetModuleByTreeId(node.id);
if (m)
{
if (m.module.fnDestroy) m.module.fnDestroy();
Ant.layout.remove('center', m.panel.getId());
m.fnDelete();
}
if (node.firstChild) node.childNodes.foreach (fn);
if (node.isLast()) return true;
}
this._tree.on('remove', function (tree, parent, node) {
if (node.event_action == 'move') return;
cr.un ('panelremoved', this._fnOnPanelRemoved, this);
Ant.layout.beginUpdate();
fn (node);
Ant.layout.endUpdate();
cr.on ('panelremoved', this._fnOnPanelRemoved, this);
}, this)


// Set up the layout
Ant.layout.beginUpdate();
Ant.layout.add('west', treePanel);
Ant.layout.getRegion('west').show();
Ant.layout.getRegion('center').show();
Ant.layout.endUpdate();
}

/**
* This function destroys the layout and does
* all needed cleanup. Must be called when module
* is about to close!
*/
p.fnDestroyLayout = function ()
{
cr = Ant.layout.getRegion('center');
cr.un ('panelremoved', this._fnOnPanelRemoved, this);
while (Ant.arrModules.length)
{
var m = Ant.arrModules[0];
if (m.module.fnDestroy) m.module.fnDestroy();
delete m.module;
Ant.layout.remove('center', m.panel);
Ant.arrModules.splice (0, 1);
}
Ant.layout.remove('west', this._treePanel);
delete this._treePanel;
delete this._tree;
}

/**
* Return tree object
*
* @return Ext.tree.TreePanel
*/
p.fnGetTree = function () { return this._tree }

/**
* Return layout object
*
* @return Ext.Ext.BorderLayout
*/
p.fnGetLayout = function () { return Ant.layout; }

/**
* Return main panel object
*
* @return Ext.ContentPanel
*/
p.fnGetPanel = function () { return this._panel; }

/**
* Opens a new module into a tab
*
* @param Object data = {
* module : Object // {name, id} of the module ???
* title : string // title of the tab
* node : Ext.tree.TreeNode // node to bind the tab to
* url : string // url to use for Ajax request
* }
*/
p.fnOpenModule = function (data)
{
if (data.node)
{
var m = Ant.fnGetModuleByTreeId (data.node.id);
if (m)
{
Ant.layout.showPanel(m.panel);
return;
}
}
else
{
var m = Ant.fnGetModule (data.module);
if (m)
{
Ant.layout.showPanel(m.panel);
return;
}
}

Ant.fnShowProgress ("Loading module...");
new AntAjaxRequest (data.url, null, function (r, self) {
if (Ant.fnIfSuccess(r)) self._fnLoadModule (r, data);
}, this);
}

/**
* Load the actual content. Create tab and etc,
*
* @access private
*/
p._fnLoadModule = function (r, data)
{
var panel = new Ext.ContentPanel(Ext.id(), {autoCreate:true, title: data.title, closable:true, fitToFrame:true, autoScroll:false})

// Create panel for content
Ant.layout.beginUpdate();
Ant.layout.add('center', panel);
Ant.layout.endUpdate();

// Create new module
Ant.activeModule = {name : data.module.name, id : data.module.id, panel : panel, treeNodeId : data.node ? data.node.id : null, node : data.node};
Ant.fnAddModule (Ant.activeModule);

Ant.targetId = panel.getId();
Ant.targetPanel = panel;

// Load
Ant.fnLoadContent (r);
}

/**
* Closes the tab
*
* @access private
*/
p._fnCloseTab = function (node)
{
if (!node.isLeaf()) return;
var m = Ant.fnGetModuleByTreeId(node.id);
if (m)
{
Ant.layout.beginUpdate();
Ant.layout.remove('center', m.panel.getId());
Ant.layout.endUpdate();
}
}

/**
* Handle the removal of the panel
*/
p._fnOnPanelRemoved = function (r, p)
{
for (i = 0; i < Ant.arrModules.length; i++)
{
m = Ant.arrModules[i];
if (m.panel == p)
{
if (m.module.fnDestroy) m.module.fnDestroy();
Ant.arrModules.splice (i, 1);
return;
}
}
}
p = null;


This is one of the modules that uses layout baseclass
/**
* This class manages the UI of the
* security module
*
* @extends AntTreeTabbedContent
* @autho Albert Varaksin
*/

/**
* Construct the class
*/
var objSecurityModule = null;
var AntSecurityModule = function ()
{
Ant.application = this;
objSecurityModule = this;

var Tree = Ext.tree;

this.fnLoadLayout ({
treeTitle : 'security',
treeWidth : 220,
tree : {
animate : true,
loader : new Tree.TreeLoader({dataUrl : strBaseUrl + 'security/gettree'}),
enableDD : false,
containerScroll : true
},
treeRoot : {
text : 'Security',
qtip : 'Manage security related settings',
allowDrop : false,
allowDrag : false,
id : 'ROOT',
icon : 'images/security.gif'
}
});

var tree = this.fnGetTree();
tree.on ('contextmenu', this.fnOnContextMenu, this);
tree.on ('dblclick', function (node) {
if (node.id.substr(0, 10) == 'USERGROUP_')
this.fnOpenUsergroup (node.id.substr(10), node)
else if (node.id == 'USERS')
this.fnOpenUsersList ();
}, this);

// for creatign unique id's for user tabs
this.usrCnt = 0;
}
Ext.extend (AntSecurityModule, AntTreeTabbedContent);
var s = AntSecurityModule.prototype;


/**
* Handle context menu
*/
s.fnOnContextMenu = function (node, event)
{
if (node.id == 'PRIVILEGES')
{
this.fnPrivilegesMenu (null, node, event.getXY());
}
else if (node.id == 'USERGROUPS')
{
this.fnUsergroupMenu (null, node, event.getXY());
}
else if (node.id.substr(0, 10) == 'USERGROUP_')
{
this.fnUsergroupMenu (node.id.substr(10), node, event.getXY());
}
else if (node.id == 'USERS')
{
this.fnUsersMenu (null, node, event.getXY());
}
}


/**
* Show context menu for usergroups
*/
s.fnUsergroupMenu = function (id, node, xy)
{
self = this;
if (id == null)
{
if (!Ant.privileges.usergroups.add) return;
var menu = new Ext.menu.Menu ({
id : 'contextMenu',
items: [
new Ext.menu.Item ({
text : 'New usergroup',
handler : function () { new AntUsergroupDialog (id, 'create', node) }
})
]
});
}
else
{
var menu = new Ext.menu.Menu ({
id : 'contextMenu',
items: [
new Ext.menu.Item ({
text : 'Open',
handler : function () { self.fnOpenUsergroup (id, node) }
}),
'-',
new Ext.menu.Item ({
text : 'New usergroup',
handler : function () { new AntUsergroupDialog (id, 'create', node) },
disabled: !Ant.privileges.usergroups.add
}),
new Ext.menu.Item ({
text : 'Edit',
handler : function () { new AntUsergroupDialog (id, 'edit', node) },
disabled: !Ant.privileges.usergroups.edit
}),
new Ext.menu.Item ({
text : 'Delete',
handler : function () {self._fnDeleteUsergroup (id, node)},
disabled: !Ant.privileges.usergroups.remove
}),
]
});
}
menu.showAt(xy);
}

/**
* Removes the usergroup
* @access private
*/
s._fnDeleteUsergroup = function (id, node)
{
Ext.MessageBox.confirm ("Delete '"+ node.text +"'", "are you sure?", function (btn) {
if (btn == 'yes') {
new AntAjaxRequest ('usergroups/remove/id/' + id, null, function (r) {
if (Ant.fnIfSuccess (r, 'Could not remove the usergroup'))
node.parentNode.removeChild(node);
})
}
});
}

/**
* Show context menu for users
*/
s.fnUsersMenu = function (id, node, xy)
{
var self = this;
var menu = new Ext.menu.Menu ({
id : 'contextMenu',
items: [
new Ext.menu.Item ({
text : 'Users list',
handler : function () {
self.fnOpenUsersList ();
}
}),
'-',
new Ext.menu.Item ({
text : 'Add new user',
handler : function () {
self.fnOpenUser ()
},
disabled: !Ant.privileges.users.add
})
]
});
menu.showAt(xy);
}

/**
* Is called on attempt to close this module
*/
s.fnOnModuleClose = function (func)
{
this.fnDestroyLayout();
func ();
}

/**
* Open Usregroup module
*/
s.fnOpenUsergroup = function (id, node)
{
if (!Ant.privileges.usergroups.view) return;
this.fnOpenModule ({
module : {name : 'usergroup', id : id},
title : node.text,
node : node,
url : 'usergroups/viewPrivileges/id/' + id
});
}

/**
* Open user module
*/
s.fnOpenUser = function (id)
{
if (!Ant.privileges.users.view) return;
if (!id)
{
this.usrCnt += 1
this.fnOpenModule ({
module : {name : 'new_user', id : this.usrCnt},
title : 'New user profile',
node : null,
url : 'users/viewProfile/'
});
}
else
{
this.fnOpenModule ({
module : {name : 'user_profile', id : id},
title : 'Modify profile',
node : null,
url : 'users/viewProfile/id/' + id
});
}
}

/**
* Open a list of users
*/
s.fnOpenUsersList = function (ug_id)
{
if (!Ant.privileges.users.view) return;
this.usrCnt += 1
this.fnOpenModule ({
module : {name : 'users_list', id : this.usrCnt},
title : 'Users',
node : null,
url : 'users/viewList/'
});
}

// Initalize
s = null;
Ant.fnInitalizer ();


and here are 2 module examples:

var AntUsergroupModule = function (modules)
{
// Register this module to layout handler
Ant.activeModule.module = this;
var id = Ant.activeModule.id;

this.url = strBaseUrl + 'usergroups/getPrivileges/id/' + id + '/';

// Create datastore
var ds = new Ext.data.Store({
proxy: new Ext.data.HttpProxy ({
url: this.url
}),
reader: new Ext.data.JsonReader({
root: 'data',
totalProperty: 'count'
},
[
{name: 'module', mapping : 'module'},
{name: 'action', mapping : 'action'},
{name: 'description', mapping : 'description'},
{name: 'state', mapping : 'state'},
{name: 'inherited', mapping : 'inherited'},
{name: 'url', mapping : 'url'}
])
});

// For formatting
var stateRenderer = function (value)
{
if (value == 'inherited') return '<span style="color: blue; font-weight: bold;">Inherited</span>'
return value == true ? '<span style="color: green; font-weight: bold;">Allowed</span>' : '<span style="color: gray">Disabled</span>';
}
var arrColors = ['black', 'green', 'blue', 'red', 'darkred'];

// Create column model
var fm = Ext.form, Ed = Ext.grid.GridEditor;
var cm = new Ext.grid.ColumnModel([
{header: "Module", width: 50, sortable: true, locked:false, dataIndex: 'module'},
{header: "Action", width: 50, sortable: true, dataIndex: 'action'},
{header: "Description", width: 75, sortable: true, dataIndex: 'description', id : 'description'},
{header: "Allowed", width: 50, align: 'center', sortable: true, dataIndex: 'state', renderer : stateRenderer, editor: new Ed(new fm.Checkbox())},
{header: "Inherited from", width: 50, sortable: true, dataIndex: 'inherited'}
]);

// Grid
this.grid = new Ext.grid.EditorGrid(Ant.targetId, {
ds: ds,
cm: cm,
autoExpandColumn: 'description',
autoSizeColumns: true,
loadMask: true,
monitorWindowResize : true,
selModel: new Ext.grid.RowSelectionModel({singleSelect:true})
});
this.grid.render();
ds.load();

// Event handlers to check if this privilege can be disabled / enabled
this.grid.on ('beforeedit', function (event){
if (event.record.data.inherited)
{
Ext.MessageBox.alert ("Notice", "This privilege is inherited from " + event.record.data.inherited)
event.cancel = true;
return;
}
});
this.grid.on ('afteredit', function (event) {
if (event.value!=event.originalValue)
{
new AntAjaxRequest (event.record.data.url + 'state/' + (event.value ? 'set' : 'remove'), null, function (r){
Ant.fnIfSuccess(r);
});
}
});

// Set actions to trap resizing
objSecurityModule.fnGetLayout().on('regionresized',this.fnResizie, this);
objSecurityModule.fnGetLayout().on('regioncollapsed', this.fnResizie, this);
objSecurityModule.fnGetLayout().on('regionexpanded', this.fnResizie, this);

// Create toolbar
var gridHead = this.grid.getView().getHeaderPanel(true);
var combo = new Ext.form.ComboBox({
store: new Ext.data.SimpleStore({
fields : ['value', 'text'],
data : modules
}),
displayField: 'text',
valueField : 'value',
width : 175,
editable : false,
mode : 'local',
emptyText : 'Select module'
})
var self = this;
var tb = new Ext.Toolbar(gridHead, [
{
text: 'Refresh',
handler : function () {
var ds = self.grid.getDataSource();
ds.reload ()
self.grid.getView().refresh();
},
cls: 'x-btn-text-icon refresh'
},
'-',
combo
]);
combo.on ('beforeselect', this.fnChangeModule, this)

var fn = function () {
Ant.fnHideProgress();
ds.un ('load', fn, this);
}
ds.on ('load', fn, this);

// Ant.fnHideProgress();
};
var p = AntUsergroupModule.prototype;

/**
* Perform cleanup
*/
p.fnDestroy = function ()
{
delete this.grid;
objSecurityModule.fnGetLayout().un('regionresized',this.fnResizie, this);
objSecurityModule.fnGetLayout().un('regioncollapsed', this.fnResizie, this);
objSecurityModule.fnGetLayout().un('regionexpanded', this.fnResizie, this);
}

/**
* perform resizing
*/
p.fnResizie = function ()
{
this.grid.getView().autoSizeColumns();
}

/**
* Change the active module in the grid
*/
p.fnChangeModule = function (combo, record, index)
{
var module = record.data.value;
var ds = this.grid.getDataSource();
ds.proxy.conn.url = this.url + (module ? 'resource/' + module : '');
ds.reload ()
this.grid.getView().refresh();
}
Ant.fnInitalizer();

and with forms
/**
* Display user profile tab
*
* @param int id
* @param array groups
*
* belongsTo is an object containing usergroups the
* user belongs to. However this is an ugly hack
* and we need a nicer way to get these values
* into the form - to follow general idea of dynamic
* form data loading.
*/
var AntUserModule = function (id, groups, belongsTo)
{
// register module
Ant.activeModule.module = this;
var target = Ext.get(Ant.targetId);
this.panel = Ant.targetPanel;

this.panel.getEl().addClass('layout-noborder');

this.id = id;

// Page layout
layout = new Ext.BorderLayout(this.panel.getEl(), {
north: {
split : false,
initialSize : 29,
border : false
},
center: {
autoScroll : false
}
});

this.layout = layout;
this.panel.on('resize', function () {layout.layout()}, layout);

var tb_panel = new Ext.ContentPanel(Ext.id(), {autoCreate:true, fitToFrame:true});
var f_panel = new Ext.ContentPanel(Ext.id(), {autoCreate:true, fitToFrame:true, autoScroll : true});

f_el = f_panel.getEl();
f_el.addClass ('tab-form');

layout.beginUpdate();
layout.add('north', tb_panel);
layout.add('center', f_panel);
layout.endUpdate();

var self = this;
var toolbar = new Ext.Toolbar (
tb_panel.getId(), [
{text : 'Save', cls:'x-btn-text-icon save', handler : function () {self.fnSaveForm();}}
]
);

var form = new Ext.form.Form ({
labelAlign : 'left',
labelWidth : 125,
reader: new Ext.data.JsonReader ({ root : 'data', totalProperty : 'count'},
[
{name : 'username', mapping : 'username'},
{name : 'disabled', mapping : 'disabled'},
{name : 'confirmed', mapping : 'confirmed'},
{name : 'real_name', mapping : 'real_name'},
{name : 'birthday', mapping : 'birthday'},
{name : 'postcode', mapping : 'postcode'},
{name : 'address', mapping : 'address'},
{name : 'company', mapping : 'company'},
{name : 'email', mapping : 'email'},
{name : 'phone_home', mapping : 'phone_home'},
{name : 'phone_work', mapping : 'phone_work'},
{name : 'phone_mobile', mapping : 'phone_mobile'},
{name : 'fax', mapping : 'fax'}
]
)
});
this.form = form;

/*
f_panel.load ({
url : strBaseUrl + 'security/userForm/',
params : {formTarget : f_panel.getId()},
scripts : true,
text : 'Please wait',
method : 'GET'
});
*/


form.column({width:500});

if (!id)
{
// Account info
form.fieldset(
{legend : 'User account'},
new Ext.form.TextField({
fieldLabel: 'Username',
name: 'username',
minLength: 4,
maxLength: 25,
allowBlank:false
}),
new Ext.form.TextField({
fieldLabel: 'Password',
name: 'password',
minLength: 4,
maxLength: 12,
allowBlank : false
}),
new Ext.form.Checkbox({
fieldLabel : 'Disabled',
name : 'disabled'
}),
new Ext.form.Checkbox({
fieldLabel : 'Confirmed',
name : 'confirmed'
})
);
}
else
{
// Account info
form.fieldset(
{legend : 'User account'},
new Ext.form.TextField({
fieldLabel: 'Username',
name: 'username',
minLength: 4,
maxLength: 25,
allowBlank:false,
disabled : true
}),
new Ext.form.Checkbox({
fieldLabel : 'Disabled',
name : 'disabled'
}),
new Ext.form.Checkbox({
fieldLabel : 'Confirmed',
name : 'confirmed'
})
);
}

// Usergroups options
form.fieldset (
{legend : 'Usergroups'}
);
for (i = 0; i < groups.length; i++)
{
eval ('set = belongsTo.ug_' + groups[i].id);
form.add (
new Ext.form.Checkbox({
fieldLabel : groups[i].name,
name : 'usergroup[' + groups[i].id + ']',
boxLabel : groups[i].description,
checked : set
})
)
}
form.end();

// genral information
form.fieldset (
{legend:'General Information'},
new Ext.form.TextField({
fieldLabel: 'Full Name',
name: 'real_name',
allowBlank:false
}),
new Ext.form.DateField({
fieldLabel: 'Date of Birth',
name: 'birthday',
allowBlank:false,
format : 'd-m-Y',
emptyText : 'dd-mm-yyyy'
}),
new Ext.form.TextField({
fieldLabel: 'Postcode',
name: 'postcode'
}),
new Ext.form.TextArea({
fieldLabel: 'Address',
name: 'address'
}),
new Ext.form.TextField({
fieldLabel: 'Company',
name: 'company'
}),
new Ext.form.TextField({
fieldLabel: 'Email',
name: 'email',
vtype : 'email',
allowBlank:false
})
);

form.fieldset(
{legend:'Phone numbers'},
new Ext.form.TextField({
fieldLabel: 'Home',
name: 'phone_home'
}),
new Ext.form.TextField({
fieldLabel: 'Work',
name: 'phone_work'
}),
new Ext.form.TextField({
fieldLabel: 'Mobile',
name: 'phone_mobile'
}),
new Ext.form.TextField({
fieldLabel: 'Fax',
name: 'fax'
})
);

form.applyIfToFields({
width:300
});

form.render(f_el);

if (id)
{
this.form.load ({url : strBaseUrl + 'users/getProfile/id/' + id});
}

Ant.fnHideProgress();
}
var p = AntUserModule.prototype;

/**
* Do clean up
*/
p.fnDestroy = function ()
{
delete this.form;
delete this.toolbar;
delete this.layout;
}

/**
* Save form data
*/
p.fnSaveForm = function ()
{
if (this.form.isValid())
{
var args = this.form.getValues(true);
if (!this.id)
{
new AntAjaxRequest ('users/addProfile/', args, this.fnCreateUser, this);
}
else
{
new AntAjaxRequest ('users/editProfile/id/' + this.id, args, this.fnCreateUser, this);
}
}
}

/**
* Request handler
*/
p.fnCreateUser = function (r, self)
{
if (Ant.fnIfSuccess(r, 'User profile could not be saved.'))
{
if (!self.id)
{
Ext.MessageBox.alert ("Profile added", "New user profile is added successfully", function (){
Ant.layout.remove('center', self.panel);
})
}
else
{
Ext.MessageBox.alert ("Profile updated", "User profile is updated successfully", function (){
Ant.layout.remove('center', self.panel);
})
}
}
else // if not successful response
{
if (r.errors)
{
var form = self.form;
r.errors.foreach (function (item){
f = form.findField (item[0]);
if (f) f.markInvalid (item[1]);
})
}
}
}


p = null;
Ant.fnInitalizer();

I hope the code is not too confusing and you can give me some advise on how to improve it and perhaps suggest a better way of doing things. I am sorry I can't host this anywhere for a live demo as the app is really big (php backend is huge) and I don't have any hosting site atm.