PDA

View Full Version : Ext.ux.TreeCommands



Ronaldo
19 Nov 2008, 2:32 PM
Hi all,

When building a tree (and a grid too) I find myself repeatingly implementing more of the same... (new, copy, delete, properties).
So here is my humble solution: Ext.ux.TreeCommands.
Before explaining it all, lets give a lot of credits to jsakalos (http://extjs.com/forum/member.php?u=2178), as I 'borrowed' a lot of tricks for manipulation the tree from his wonderfull filetreepanel.

The problem this set of plugin tries to solve is to be able to create a fully fledged tree panel with

Minimal configuration
Reusable code
Consistent icon/language usage The drag/drop part and the new and copy actions in a tree are not that straightforward. So I'd like to solve them once and for all if possible.

I'm using the command pattern, which theoretically allows for undo/redo functionality. I've been working on that, but in this version it's disabled.

Here's my tree panel code:



Ext.bb.PageTreePanel = Ext.extend(Ext.tree.TreePanel, {
baseUrl :Ext.bb.Url.pageprefix,
autoScroll :true,
rootVisible :true,
loadMask :true,
enableDD :true,
containerScroll :true,
selectOnEdit: false,
root : {
id :'0',
text :'Pages',
draggable :false,
type :'folder',
expand :true
},
// Would allow undo/redo functionality if not disabled (and not only here)
//cmdStack: new Ext.ux.util.CommandStack(),
plugins: [
new Ext.ux.tree.DragDropPlugin({cmdCfg:{baseUrl: '/page/'}}),
new Ext.ux.tree.ContextMenuAdapterPlugin({
cmdCfg: {baseUrl: '/page/'},
items:[
Ext.ux.tree.CtmItemNew,
'-',
Ext.ux.tree.CtmItemRename,
Ext.ux.tree.CtmItemCopy,
Ext.ux.tree.CtmItemRefresh,
'-',
Ext.ux.tree.CtmItemDelete,
'-',
Ext.ux.tree.CtmItemProperties
]
}),
new Ext.ux.tree.ToolbarAdapterPlugin({
cmdCfg: {baseUrl: Ext.bb.Url.pageprefix},
items:[
Ext.ux.tree.TbItemNew,
Ext.ux.tree.TbItemProperties,
'-',
Ext.ux.tree.TbItemRefresh,
]
})
],
initComponent : function() {
this.loader = new Ext.tree.TreeLoader( {
dataUrl :this.baseUrl+'getChildren'
});

// This *must* be set here in order to let the toollbar adapter plugin work
this.tbar = new Ext.Toolbar();

Ext.bb.PageTreePanel.superclass.initComponent.call(this);

this.treeEditor = new Ext.tree.TreeEditor(this, {
...
});

this.on("dblclick", this.onProperties, this);
this.on("beforeexecutecmd", this.onBeforeExecuteCmd, this);
},
createContextMenu: function(menu) {
// Allows specific menu additions apart from the plugins
},
onBeforeExecuteCmd: function(tree, req) {
// Change the request parameters before executing the ajax command
if(req.node)
req.params.nodeType = req.node.attributes.entityName;
},
/** s is an array of selected nodes */
onProperties : function(s) {
// Called from the Ext.ux.tree.PropertiesCommand
var node = Ext.isArray(s) ? s[0] : s;
if(!node) return;
this.main.createPageForm({id: node.id},{mainTreeId: node.id});
},
/** Utility function used in the commands
to make sure the selection is always an (optionally empty) array of selected nodes */
getSelection: function() {
var s=null, sm = this.getSelectionModel();
if(typeof sm.getSelectedNode=='function') {
s = sm.getSelectedNode();
s = s ? [s] : [];
} else {
s = sm.getSelectedNodes();
}
return s;
},
selectNodeById : function(id, clearOther) {
if(clearOther == true)
this.getSelectionModel().clearSelections();

var node = this.getNodeById(id);
if(node) {
node.select();
node.ensureVisible();
}
return node;
}
});
There are 3 plugins:
Ext.ux.tree.DragDropPlugin - Plugin that completely manages drag and drop functionality.Use the standard onbeforedrag/drop events to cancel events as you would do normally.

And the
Ext.ux.tree.ContextMenuAdapterPlugin
Ext.ux.tree.ToolbarAdapterPluginWhich adapt their items, which are defined as static items:



Ext.ux.tree.CtmItemNew = {
cmdName: 'newNode',
text: 'New',
iconCls:'iconNew',
getCmd: function(cfg) {
return new Ext.ux.tree.NewCommand(cfg);
},
isEnabledFn: Ext.ux.tree.isEnabledFn
}
For clarity, here's the toolbar version:


Ext.ux.tree.TbItemCopy = {
cmdName: 'copyNode',
// text: 'Copy', // Don't want no text for this button
iconCls:'iconCopy',
getCmd: function(cfg) {
return new Ext.ux.tree.CopyCommand(cfg);
},
isEnabledFn: Ext.ux.tree.isEnabledFn
}These items are defined in Ext.ux.tree.ContextMenuCommandItems.js and Ext.ux.tree.ToolbarCommandItems.js.
I've duplicated these items as a contextmenu item can have other text (or a shortcut key) than the toolbar item. Moreover, this way they're translatable.

These items are cloned when necessary and can be reused in every tree you need.
Basically, clicking on a toolbar or context menu item triggers an adapter event listener,
which check if the button or item has a getCmd function.
Then, a new object is instantiated that carries out the request (ie new node, delete node, copy node etc).

The adapters provide functionality to enable/disable items based on the selection.
All commands use JSON, and expect a result like

{success:true}

On some (like copy command), if you return

{success:true, data=[{id:10, name:'I am a new node', otherattribute: 'hello world'}]}

will also update the new node with the id, and set node.attributes.otherattribute to 'hello world'.

Returning errors like

{success:false, errors:[{id:null, msg:'Duplicate record'}]}

will cause the command to undo what it has already done (like adding a new node when copying) and display the error message.

The baseURL (config) is passed to all commands, so the new command would go to
/page/createNode

and its full request is something like:

/page/createNode?cmdName=createNode&name=New node

The /page/ part of the url isDefined in the plugin definition


new Ext.ux.tree.ContextMenuAdapterPlugin({
cmdCfg: {baseUrl: '/page/'},
items:...and "createNode" is defined as url property in the Ext.ux.NewCommand.js

Oh, the code can be downloaded at http://www.twensoc.nl/Ext.ux.TreeCommands 0.9.rar (http://www.twensoc.nl/Ext.ux.TreeCommands%200.9.rar)
Even though some code comments state LGPL, I feel free to change that license later on,
As I'm not sure yet. You can use this version anyway as you like, except that you can't sell it ;) That is, use it in any (commercially) project, but you can't sell this code as part of any other toolkit/code/extensions code.

I'm working on more documentation :) As always, remarks/comments/discussion is welcome.

Ronaldo

ThorstenSuckow
19 Nov 2008, 4:37 PM
Sweet. I'm using something similiar and planned to decouple it for reusing in other trees. Since I've not looked into your code let me ask you right away: Does all of the functionality work in a tree which nodes are lazy loaded? It took me some time to apply the command pattern to nodes which haven't been expanded (i.e. loaded from the server) which basically meant you'd have to take several actions/events into account before the requested command/action could be pocessed (such as appending a node to a node which is not yet fully expanded - checking for duplicate nodes etc.).
It looks promising the way you describe it. Mapping and manipulating server side datastructure (take the filesystem as an example) could become really easy considering your extension.

Edit:
Since you wrote that you are still working on the command pattern - let me know if you need some code regarding this - it's already implemented in my component but needs some decoupling so it can be easily used within other trees.

Ronaldo
20 Nov 2008, 12:37 AM
Right now nodes need to be loaded in order to do something with them, as a node should be selected to be copied for example.
So I'm not exactly sure what you mean by doing actions based on unloaded nodes.

The rename undo/redo functionality worked, but then I saw problems with unloaded nodes, records changed by another user etc. I plan on solving these issues, but one step at the time.
The idea though is that for an command to be executed a node must be loaded. The command doesn't store the node but rather it's id (So every node must have a unique id, as it is now)
On undoing a command, we need to be sure that the node with that id still exists, and that the undo can be carried out server side.

Executing a new node command to a node that's not expanded/loaded yet will cause that (parent)node to be loaded first.

I hope this more or less answers your question...

Ronaldo



For clarity, here's the rename command. Note that commented lines contain the undo/redo functionality.



Ext.ux.tree.RenameCommand = Ext.extend(Ext.ux.tree.Command, {
cmdName: 'renameNode',
url: 'renameNode',
run: function(tree,selection) {
this.tree = tree;
var node = selection[0];
if(node && node.getDepth()>0) {
var editor = this.tree.treeEditor;
editor.on({complete:{scope: this, single: true, fn: this.onEditCompleted}});
(function(){editor.triggerEdit(node);}.defer(100));
}
},
// redo: function() {
// this.undo();
// },
// undo: function() {
// var c = this.undoCfg;
// if(!c)
// return;
// var node = this.tree.selectNodeById(c.nodeId, true);
// if(!node)
// return;
// node.setText(c.oldName);
// this.execute({node:node, oldName: c.newName}, {node: node.id, oldname: c.newName, newname: c.oldName});
// },
onEditCompleted: function (editor, newName, oldName) {
if(editor.creatingNew || newName==oldName)
return;
var node = editor.editNode;
this.execute({node:node, oldName: oldName}, {node: node.id, oldname: oldName, newname: newName});
},
onSuccess: function(req, dr, r) {
// this.undoCfg = {
// nodeId: req.node.id,
// oldName: req.oldName,
// newName: req.node.text
// };
Ext.ux.tree.RenameCommand.superclass.onSuccess.call(this, req, dr, r);
},
onFailure: function(req, dr, r) {
delete(this.undoCfg);
if(req.node)
req.node.setText(req.oldName);
Ext.ux.tree.RenameCommand.superclass.onFailure.call(this, req, dr, r);
}
});


And this is the basis object for all commands:



/**
* @class Ext.ux.TreeCommand
* @Author Ronald van Raaphorst
* @Version 0.9
*/
Ext.ux.tree.Command = function(cfg){
Ext.apply(this,cfg);
};

/** @private */
Ext.ux.tree.Command.prototype = {
/**
* @cfg {String} baseUrl If the baseUrl property is set,
* the full url called is the concatenation of the baseUrl and the url (baseUrl + url).
* Defaults to null (not set).
*/
baseUrl: null,
/**
* @cfg {String} url The url to request when the command is executed. If the baseUrl property is set,
* the full url called is the concatenation of the baseUrl and the url (baseUrl + url).
*/
url: 'unknown url',
/**
* @cfg {String} cmdName Name of the command to send as parameter of the request.
*/
cmdName: 'unknown treecommand',
/**
* @cfg {String} method Method used to call the url. Either 'post' or 'get'.
*/
method: 'post',
/**
* @cfg {Integer} maxMsgLen
*/
maxMsgLen: 80,
/**
* onDestroy listener that deletes every object reference in this instance.
*/
onDestroy: function() {
for(var p in this) {
if(typeof this[p] == 'object')
this[p] = null;
}
},
/**
* Executes the ajax request to the server. This method is called by most commands that inherit from
* the this object. This method sets the following request properties, set by the commands that inherit from
* this object.
* <br/><p>Options set</p><ul>
* <li><b>url</b> Url to call. This url is composed of the baseUrl concatenated with the url</li>
* <li><b>method</b> Method used to call the url (post or get).</li>
* <li><b>scope</b> The scope is set to this object instance.</li>
* <li><b>callback</b> Set to the callback method of this object.</li>
* </ul>
* <br/><p>Parameters set</p><ul>
* <li><b>cmdName</b> Sets the cmdName parameter to send as request parameter.</li>
* </ul><br/>
* @param {Object} options Basic options linked to the ajax request.
* @param {Object} params Parameters to be passed with the ajax request to the server.
*/
execute : function(options, params) {
this.lastParams = params;
var req = Ext.apply({
url: this.baseUrl ? this.baseUrl + this.url : this.url,
method: this.method,
scope: this,
callback :this.callback,
params: Ext.apply({
cmdName: this.cmdName
}, params)
}, options);
if(this.tree.fireEvent('beforeexecutecmd', this.tree, req)===false)
return;
if(this.eventName) {
if(this.tree.fireEvent('before'+this.eventName, this.tree, req)!==false)
Ext.Ajax.request(req);
} else
Ext.Ajax.request(req);
},
/**
* Placeholder for the onSuccess handler called by the callback method.
* Override this method if you need to perform specific command logic when the server request
* has returned succesfully.
* When overriding this method, be sure to call this one too as it sends the after event on behalf of the tree.
* @param {Object} req The original ajax request.
* @param {Object} dr The decoded response.
* @param {Object} response The complete ajax response.
*/
onSuccess:function(req, dr, response) {
if(typeof this.undo == 'function' && this.tree && this.tree.cmdStack && !this.addedToCmdStack) {
this.addedToCmdStack = true;
this.tree.cmdStack.add(this);
}
if(this.eventName)
this.tree.fireEvent('after'+this.eventName, this.tree, req, response);
},
/**
* Placeholder for the onFailure handler called by the callback method.
* Either the server has returned an error code (ie. 404) or the server
* responded with success=false. (In JSON: {success:'false'}).
* Override this method if you need to perform specific command logic when the server request
* has returned unsuccesfully.
* When overriding this method, be sure to call this one too as it shows the error in a Messagebox.
* @param {Object} req The original ajax request.
* @param {Object} dr The decoded response.
* @param {Object} response The complete ajax response.
*/
onFailure:function(req, dr, response) {
if(dr && dr.errors) {
this.showError('Error', dr.errors);
} else {
this.showError('Error', response.responseText, true);
}
},
/**
* Utility function to show error messages returned by the server.
* @param {String} title Title of the error message box.
* @param {Mixed} msg Single message as string, or an Array of messages to be displayed.
* @param {Boolean} fullMsgView . Boolean value defining wheter to limit the message length.
* Defaults to true.
*/
showError: function(title, msg, fullMsgView) {
var m=null, fm=Ext.util.Format.ellipsis;
if(Ext.isArray(msg)) {
m='';
for(var i=0;i<msg.length;i++) {
if(i>0) m += '<br/>';
m += fm(msg[i].msg, this.maxMsgLen);
}
}
Ext.Msg.show({
title:title || this.errorText,
msg: m ? m : !fullMsgView ? fm(msg, this.maxMsgLen) : msg,
fixCursor:true,
icon:Ext.Msg.ERROR,
buttons:Ext.Msg.OK,
minWidth: 360
});
},
/**
* Standard callback method for the ajax request.
* @param {Object} req The original ajax request.
* @param {Boolean} success Boolean value indicating whether the request was succesfull.
* @param {Object} response The complete ajax response.
*/
callback:function(req, success, response) {
if(success !== true) {
this.onFailure(req, null, response);
} else {
try {
var dr = Ext.decode(response.responseText);
if(dr.success === true)
this.onSuccess(req, dr, response);
else
this.onFailure(req, dr, response);
}
catch(ex) {
this.onFailure(req, null, response);
}
}
},
cmdToStack:function() {
if(this.tree && typeof this.tree.cmdStack=='object') {
this.tree.cmdStack.add(this);
}
},
/**
* Update node data returned from the server.
* Some commands may optionally return extra data when executing the command. The {@link Ext.ux.tree.NewCmdPlugin}
* for example expects the new id to be returned.
* @param {Object} n The node to be updated.
* @param {Object} d The data returned from the server.
*/
updateNodeData: function(n, d){
if(!n || !d) return;
n.id = d.id;
for(var p in d)
n.attributes[p] = d[p];
if(d.iconCls)
n.iconCls(d.iconCls);
if(d.leaf)
n.leaf = d.leaf;
}
}

Ronaldo
20 Nov 2008, 12:40 AM
Edit:
Since you wrote that you are still working on the command pattern - let me know if you need some code regarding this - it's already implemented in my component but needs some decoupling so it can be easily used within other trees.

I'm using the code in a real project now, and it works for me, so the 'command pattern' is ok.
If you have any suggestions, other commands, I'd be happy to hear them and improve the code anyway I can.

ThorstenSuckow
20 Nov 2008, 1:33 AM
I'm using the code in a real project now, and it works for me, so the 'command pattern' is ok.
If you have any suggestions, other commands, I'd be happy to hear them and improve the code anyway I can.

Looking at your code I think we both follow similiar approaches. Since you're already working with this in a project, I guess you're a step ahead of me ;)

divxer
20 Nov 2008, 4:36 PM
demos or some examples to show the usage? Thanks!

Ronaldo
21 Nov 2008, 6:31 AM
demos or some examples to show the usage? Thanks!

A demo will show nothing more than a standard treepanel with a toolbar and a contextmenu. The power is in the configuration, see the bold text in the first post:



Ext.bb.PageTreePanel = Ext.extend(Ext.tree.TreePanel, {
...
plugins: [
new Ext.ux.tree.DragDropPlugin({cmdCfg:{baseUrl: '/page/'}}),
new Ext.ux.tree.ContextMenuAdapterPlugin({
cmdCfg: {baseUrl: '/page/'},
items:[
Ext.ux.tree.CtmItemNew,
'-',
Ext.ux.tree.CtmItemRename,
Ext.ux.tree.CtmItemCopy,
Ext.ux.tree.CtmItemRefresh,
'-',
Ext.ux.tree.CtmItemDelete,
'-',
Ext.ux.tree.CtmItemProperties
]
}),
new Ext.ux.tree.ToolbarAdapterPlugin({
cmdCfg: {baseUrl: Ext.bb.Url.pageprefix},
items:[
Ext.ux.tree.TbItemNew,
Ext.ux.tree.TbItemProperties,
'-',
Ext.ux.tree.TbItemRefresh,
]
})
],
...


Without these plugins, you'd need to create your own contextmenu, toolbar, and all their items, and their handlers.
Moreover, you'd need to implement all the commands over and over again in every tree.

I'd be happy to answer more specific questions.

Ronaldo

Frenky
15 Dec 2008, 3:41 AM
lively demo?
thanks in advance