PDA

View Full Version : Ext.ux.tree.SyncedTreeLoader



crp_spaeth
9 Feb 2009, 4:51 AM
I' am developing an multi User Application and I needed a kind of Sync mechanism for the Tree. May two or more User working in the same Tree and delete add or update Treenodes.

In the Current Version of the Tree you can ask the tree to re-request the Rootnode.
But there is no Way to just update the Tree...

So I decided to implement my own Updatemechanism.
I decided to Extend the Standart Ext.tree.Treeloader and to override the requestData and proccessresponse functions to handle updates.

You may Take a look into the code Documentation for more details...


Ext.ns('Ext', 'Ext.ux', 'Ext.ux.tree');

/**

* @class Ext.ux.tree.SyncableTreeLoader
* @extends Ext.tree.TreeLoader
*
*
* A SyncableTreeLoader provides the same functionality as its Baseclass (Ext.tree.Treeloader) plus the
* possibility of lazy updating an existing Structure of TreeNodes from a specified Url.
*
* The response must be a JavaScript Object definition whose Members are (by default) "deleted", "updated", "added"
*
* "deleted" contains a simple Javascript Array definition of the TreeNode-Id that should be deleted
*
* "updated" contains a JavasScript Array defintion contaioning the Object definition of the updated
* TreeNodes with its new attribute properties
*
* "added" contains a JavaScript Array of definition containing the Object definition of the treeNodes
* that should get Added to the Tree. Cause the Tree needs to know where to insert/append the TreeNode,
* the definition of the Node needs to contain an Attribute with the Id of its Parentnode "Parentnode"
* and Optional its Presilbing.
*
* A possible Response looks like this:

* <pre><code>
{
"deleted" : ['nodeId123'],
"updated" : [{
"leaf" : false,
"id" : "nodeId342",
"children" : [{
"leaf" : false,
"id" : "node12314",
"children" : [],
"attr" : {
"preSilbing_id" : null,
"parent_id" : "nodeId342",
"Dokumente" : "0",
"leaf" : false,
"text" : "childs text"
}
}
],
"attr" : {
"parent_id" : "parentNodeId",
"preSilbing_id" : null,
"text" : "Node Text",
"leaf" : false
}
}],
"added" : [{
"leaf" : false,
"id" : "nodeId555",
"children" : [{
"leaf" : false,
"id" : "node12314",
"children" : [],
"attr" : {
"preSilbing_id" : null,
"parent_id" : "nodeId342",
"Dokumente" : "0",
"leaf" : false,
"text" : "childs text"
}
}
],
"attr" : {
"parent_id" : "RootNodeID",
"preSilbing_id" : "FirstChildsId",
"text" : "Node Text",
"leaf" : false
}]
}
</code></pre>
* @author Martin Spaeth
* @copyright (c) 2008, by Martin Spaeth
*/
Ext.ux.tree.SyncableTreeLoader = Ext.extend( Ext.tree.TreeLoader, {

/**
* @cfg {string} updateParamName
* Parametername for the requesttype
*/
updateParamName: 'request',

/**
* @cfg {string} updateParamValue
* value of the Parameter to show the server that this is an updaterequest
*/
updateParamValue: 'update',

/**
* @cfg {String} updatedName
* Name of the array containing the nodes that should get updateted
*/
updatedName: 'updated',

/**
* @cfg {String} deletedName
* Name of the array containing the Id's of the nodes that should get deleted
*/
deletedName: 'deleted',

/**
* @cfg {String} addedName
* Name of the array containing the nodes that should get added
*/
addedName: 'added',

/**
* @cfg {String} parentIDAttr
* name of the attribute containing the parentId of a node
*/
parentIDAttr: 'parentIdAttr',
/**
* @cfg {String} preSilbAttr
* name of the attribute containing the preSilbingId of a node
*/
preSilbAttr: 'preSilbingIdAttr',

clearOnLoad : true,

/**
* @cfg tree {Ext.tree.Tree} the Tree this Treeloader is instantiated for
*/
tree: undefined,

/**
* Create a String containing all needed Request Parameters
* @param {Ext.tree.TreeNode} node
* @param {boolean} update
* @return {String} Parameterstring for a Request z.b. node=432&updete=true
*/
getParams: function(node, update){
var buf = [], bp = this.baseParams;
for(var key in bp){
if(typeof bp[key] != "function"){
buf.push(encodeURIComponent(key), "=", encodeURIComponent(bp[key]), "&");
}
}
buf.push("node=", encodeURIComponent(node.id));
if(update){
// adding the updateparameter
buf.push("&"+this.updateParamName+"=", encodeURIComponent(this.updateParamValue));
}
return buf.join("");
},

/**
* Load an {@link Ext.tree.TreeNode} from the URL specified in the constructor.
* This is called automatically when a node is expanded, but may be used to reload
* a node (or append new children if the {@link #clearOnLoad} option is false.)
*
* it now has the possibility to Update a node by requesting the data as an updaterequest
* @param {Ext.tree.TreeNode} node
* @param {Function} callback
* @param {boolean} update should the Request handled as an Update request?
*/
load : function(node, callback, upload){
if(upload === true) {
// call the Request Function with the upload flag == true
this.requestData(node, callback, upload);
} else {
// just call the normal load Function (it will then call the Request function)
Ext.ux.tree.SyncableTreeLoader.superclass.load.apply(this, arguments);
}

},


/**
* RequestData Function with the possibility to fire an updaterequest
* @param {Ext.tree.TreeNode} node the Node for wicht the data should be requested
* @param {function} callback a callbackfunction
* @param {boolean} update is it an updaterequest
*/
requestData : function(node, callback, update){
if(this.fireEvent("beforeload", this, node, callback) !== false){
this.transId = Ext.Ajax.request({
update: update,
method:this.requestMethod,
url: this.dataUrl || this.url,
success: this.handleResponse,
failure: this.handleFailure,
scope: this,
argument: {callback: callback, node: node},
params: this.getParams(node, update)
});
}else{
// if the load is cancelled, make sure we notify
// the node that we are done
if(typeof callback == "function"){
callback();
}
}
},

/**
* the function that processes the Response an handles all the Update logic
* @param {Object} response from the Server
* @param {Ext.tree.TreeNode} node The Node the Response is for
* @param {function} callback
*/
processResponse : function(response, node, callback){

var json = response.responseText;
var update = response.options.update;
var tree = node.getOwnerTree();

try {
var o = eval("("+json+")");
// is the response part of an update request?
if(update){

// delete Nodes
var del = o[this.deletedName];
if(del && del.length){
for(var i = 0; del.length > i; i++){
var delNodeId = del[i];
tree.getNodeById(delNodeId).remove();
}
}

// Add all Nodes from the added Array Responsed by the server
var added = o[this.addedName];
if(added && added.length){
for(var i1 = 0; added.length > i1; i1++){
var addedNode = added[i1];

/**
* @type Ext.tree.TreeNode
*/
var parent = tree.getNodeById(addedNode.attr[this.parentIDAttr]);
var preSilbing = tree.getNodeById(addedNode.attr[this.preSilbAttr]);

parent.beginUpdate();
// Append the Node to the Parent or Insert it after the presilbing.
if(!preSilbing){
parent.appendChild(this.createNode(addedNode));
} else {
parent.insertBefore(this.createNode(addedNode), preSilbing.nextSibling);
}

parent.endUpdate();
}
}

// Update all the nodes the Update array contains
var updated = o[this.updatedName];
if(updated && updated.length){
for(var i2 = 0; updated.length > i2; i2++){
var updatedNode = updated[i2];

/**
* @type Ext.tree.TreeNode
*/
var toUpdate = tree.getNodeById(updatedNode.id);

toUpdate.beginUpdate();
// toUpdate.updateNode(updatedNode);

// update should only handle the attributes of a node nor the children
if(updateNode.children) {
delete updateNode.children;
}

Ext.apply(toUpdate, updateNode);


// update the text of the node
if(updateNode.text){
toUpdate.setText(updateNode.text);
}

// replace the Icon
if(updateNode.iconCls){
Ext.fly(toUpdate.getUI().getIconEl()).replaceClass(oldIconCls, updateNode.iconCls);
toUpdate.iconCls = updateNode.iconCls;
}

// replace the quicktip
if(updateNode.qtip) {
var ui = toUpdate.getUI();
if(ui.textNode.setAttributeNS){
ui.textNode.setAttributeNS("ext", "qtip", updateNode.qtip);
if(updateNode.qtipTitle){
ui.textNode.setAttributeNS("ext", "qtitle", updateNode.qtipTitle);
}
}else{
ui.textNode.setAttribute("ext:qtip", updateNode.qtip);
if(updateNode.qtipTitle){
ui.textNode.setAttribute("ext:qtitle", updateNode.qtipTitle);
}
}
}

toUpdate.endUpdate();
}
}

if(typeof callback == "function"){
callback(this, node);
}
}
else {
node.beginUpdate();
for(var i = 0, len = o.length; i < len; i++){
var n = this.createNode(o[i]);
if(n){
node.appendChild(n);
}
}
node.endUpdate();
if(typeof callback == "function"){
callback(this, node);
}
}

}catch(e){
this.handleFailure(response);
}
}
});

alex-t.de
10 Feb 2009, 2:42 AM
Hey Martin,

thank you very much for sharing this code. It is a very elegant solution, it looks great.

I have to test it for my use case. I use Ext.tree for the presentation and the operations on a structure tree. In addition I use the tree with Ext.tree.ColumnTree and Ext.tree.AsyncTreeNode.

I'm not satisfied with my first quick'n'dirty solution for the update logic. I hope you're okay I'm using your SyncableTreeLoader. I'll report of my tests.



My first impression is...

There are some changes to do to get this running with the backend I use. There is a rails app on the server and this provides a tree of an 'acts_as_nested_set' model. I hope someone understand what I mean.

The output of the method SyncableTreeLoader.getParams(...) should contain a timestamp of the last update. This means that the server response has to contain an update timestamp and that is stored in a member var in SyncableTreeLoader.processResponse(...).

I think that I will need this update timestamp on the server side to collect all changes since the last update. That seems to be easy.

Ok, we got deleted, updated and added nodes.

One more thing: Moved nodes.
After moving a node by another user the server response could contain this node as to be deleted and also as to be added at the new location. The rails app should store every move of nodes incl. a timestamp in a separate list, so the update response can be generated.
I realise that the deleted nodes should also be noted in a special list.

This is surely the first intuitive solution. Another solution could use the TreePanel method movenode : ( Tree tree, Node node, Node oldParent, Node newParent, Number index ).



Questions:

- You are yousing TreeNode.beginUpdate(), do you know, why this is not in the api docs? First I thought that was depricated, but the sources say its all right.

- How do you start an update?


Best regards

Alex

crp_spaeth
11 Feb 2009, 8:51 AM
Hi Alex,

In our environment there is no need for a "Moved" Object cause as you did expect we will add a moved Node to the deleted List and we will Add it to the Added List to...

For a Move Implemantation You should look up how a Move is Handled in Drag&Drop (I think this is the Fastest Way since Its written by the Ext Team :D )

I have no Use for that and got a lot things to do for now so I can't do that for you :(

About the other Question. I override the load method so it has an extra parameter (the third one) called update gives you the possibility to ask the server for an update response

see the updated Forms

alex-t.de
12 Feb 2009, 12:13 PM
Oh, I meant to post some extra code for the move operation if I find a nice solution for this.

And I've to ask one more time how the update mechanism is called. I try to explain how my first naive solution looks like. First there is a timer wich loads a list of node_IDs that has to be reloaded. Because the tree contains AsyncTreeNodes, this method is already usable. For example, if there is a Node '123' that has to be updated, I had to call the reload method of that parent node.

So is the new load method called for every node every n seconds? Or did I miss something?


Best regards

Alex

crp_spaeth
12 Feb 2009, 3:18 PM
Okay just to clarify the Load mechanism...

If you would like to use this update mecanism you will call the load-method with a third parameter: true

Since the update part of the load method will take care of all the Nodes of the response from the Server you will just need to call the load method on the Rootnode not for every Node.

So you will end up with something like this:




var tree = new Ext.tree.TreePanel({
...
loader: new Ext.ux.tree.SyncedTreeLoader( {
...
url:"somepage.php"
...
}),
...
});

...

// the code below should be called after the Tree has already done its initial Load.
// So there are a few Nodes in the Tree already load (at least the rootNode)

// creating a task that load the Tree with parameter update every second.
var task = {
run: function(){
tree.loader.load(tree.getRootNode(), null, true);
},
interval: 1000//1 second
}

// creation a task runner and run the task
var runner = new Ext.util.TaskRunner();
runner.start(task);



So whats happening here?

We have a Tree that uses our SyncedTreeLoader as its Loader.

Now we have the ability to call the load Method with an extra parameter (update Parameter) so that the same Request to the Server is made like if we called the normal load methode but with the small difference that in the Request will have an extra parameter in the request ( by default "&update=update" but configurable via updateParamName and updateParamValue)

So when we just have a normal call with an extra Parameter you will ask yourself where happens all the "Magic" :D:D:D

The SyncedLoader expects the in the documentation descriped data from the server now.

And Since The request has the update parameter in its Properties we can ask for that:


processResponse : function(response, node, callback){

var json = response.responseText;
var update = response.options.update;
var tree = node.getOwnerTree();

try {
var o = eval("("+json+")");
// is the response part of an update request?
if(update){


So now if the Request was an Updaterequest we will handle the delete things first:


// delete Nodes
var del = o[this.deletedName];
if(del && del.length){
for(var i = 0; del.length > i; i++){
var delNodeId = del[i];
tree.getNodeById(delNodeId).remove();
}
}


after that we Add all The nodes that the response contain in the Array under the Properties "added": [...



// Add all Nodes from the added Array Responsed by the server
var added = o[this.addedName];
if(added && added.length){
for(var i1 = 0; added.length > i1; i1++){
var addedNode = added[i1];

/**
* @type Ext.tree.TreeNode
*/
var parent = tree.getNodeById(addedNode.attr[this.parentIDAttr]);
var preSilbing = tree.getNodeById(addedNode.attr[this.preSilbAttr]);

parent.beginUpdate();
// Append the Node to the Parent or Insert it after the presilbing.
if(!preSilbing){
parent.appendChild(this.createNode(addedNode));
} else {
parent.insertBefore(this.createNode(addedNode), preSilbing.nextSibling);
}

parent.endUpdate();
}
}


So, as you can see, we extract the presilbing or the parent Infomation from all the added Nodes and add all the nodes to the Tree based on that information.




Last but not least we Update the Information for those nodes whose Information (Attributs not the possition in the Tree) has Chagend by the following Lines:


// Update all the nodes the Update array contains
var updated = o[this.updatedName];
if(updated && updated.length){
for(var i2 = 0; updated.length > i2; i2++){
var updatedNode = updated[i2];

/**
* @type Ext.tree.TreeNode
*/
var toUpdate = tree.getNodeById(updatedNode.id);

toUpdate.beginUpdate();
// toUpdate.updateNode(updatedNode);

// update should only handle the attributes of a node nor the children
if(updateNode.children) {
delete updateNode.children;
}

Ext.apply(toUpdate, updateNode);


// update the text of the node
if(updateNode.text){
toUpdate.setText(updateNode.text);
}

// replace the Icon
if(updateNode.iconCls){
Ext.fly(toUpdate.getUI().getIconEl()).replaceClass(oldIconCls, updateNode.iconCls);
toUpdate.iconCls = updateNode.iconCls;
}

// replace the quicktip
if(updateNode.qtip) {
var ui = toUpdate.getUI();
if(ui.textNode.setAttributeNS){
ui.textNode.setAttributeNS("ext", "qtip", updateNode.qtip);
if(updateNode.qtipTitle){
ui.textNode.setAttributeNS("ext", "qtitle", updateNode.qtipTitle);
}
}else{
ui.textNode.setAttribute("ext:qtip", updateNode.qtip);
if(updateNode.qtipTitle){
ui.textNode.setAttribute("ext:qtitle", updateNode.qtipTitle);
}
}
}

toUpdate.endUpdate();
}
}


You can see for some of the attributes of the node there still needs do be some special handling done. So we apply all new Attributes to the old one (may we should add a flag to delete the old attributes first?) and after that we Update those special Attributes like the Text the icon and the tooltip.

I hope this will help you to get it work!

alex-t.de
14 Feb 2009, 5:50 AM
Thank you Martin!

I was unsure, but I guessed, that you have to run the update method on the root node. You've explained it well to me.

OT: Last night, I was thinking about my experiences with earlier projects. About 10 years ago I've coded a structure tree panel with html and php. In view of the usability it sucks. BTW javascript was out at that time. About 7 years ago I've coded a structure tree panel in Flash+XML+PHP. It was easy to use, easy to update, no extra code needed. But the rest of the webapp sucks, flash was not ready for the big apps. 6 years ago I've started with Java. The TableTree was a spasm, but finally it worked. Updating is still a problem. There are at least a handful Java bugs wich made the work on the tree very hard. If you're familiar with Java Trees or Swing, you know that you cannot fix some bugs at your own, because there are too many internal classes declared as private.
Anyway, I'm here now and I need a usable tree structure, wich is very reliable. And since I've switched to Macs I've thought about a column view like that one in the Finder. I've made a conception for a prototype in Ext JS and I'll try to do a nice extension. For now I can say that a lot of the operations a very easy to code. Updates are no big deal. The only unclear task is to manage the lists in a scrollable panel. But that should not be a big problem.
And I've some ideas to do the column view a bit more useful as a web structure tree panel. I think the finder does a great job with files and folders, but my web app has to do a great job on other demands.

So I've a lot of prototyping to do. Thanks for your help!




Best regards

Alex

hitekshu
22 Dec 2010, 1:53 AM
Hey crp_spaeth,

i also need a tree that is synced between 2 or more users, can you give me a complete example package with js and php code involved for this. Only if its not a trouble for you.

Thanks,
Hitekshu