PDA

View Full Version : Ext.tree.TreePanel expandPathById() method to force render of a specific node



murrah
13 May 2009, 5:29 AM
Hi,

There have been a number of posts regarding the issue of tree node objects not being available until the node is rendered and the problem that can cause when another component has a tree node id and it wants to access that node in the tree.

ie aNode = myTree.getNodeById('my node id'); returns undefined if the node has not yet been rendered.

There have been a few solutions offered covering various scenarios:
http://extjs.com/forum/showthread.php?p=73642#post73642
http://extjs.com/forum/showthread.php?p=241198

One solution if you have a smallish tree is to root.expand(true) to force the rendering of all nodes but that becomes problematic for large tree or for where you want to maintain tree state on loading (eg http://extjs.com/forum/showthread.php?t=9025&page=3)

Since there are the obvious advantages to having a lazy rendered tree and state management, I wrote the following little extentions to provide a solution that might be useful for others. The following works for an AIR app that has a tree store based on a sqLite db. However, it could easily be adapted for other types of tree stores/loaders.

The basic idea is to use the tree store/loader to make a path to the required node then use expandPath(pathStr) to render the node.

First the treePanel extention:

Ext.override(Ext.tree.TreePanel, {
expandPathById : function(id) {
// Take the node id passed in, make a path from the data in the store, then expand that path
// See if the node is already rendered. If so, just expand it
var node = this.getNodeById(id);
if (node) {
var path = node.getPath();
this.expandPath(path);
return true;
} else {
// The node with that id isnt rendered yet. Get a path to the node using the data from the store
var path = this.getLoader().store.getTreeNodePath(id); // New method. see below

if (path != '') {
// Expand that path
this.expandPath(path);
return true;
} else {
// Couldnt make a path so return false
return false;
}

}

}

});

then the Store extension. The code above calls the getTreeNodePath() method below:

tx.data.ProjectNodeStore = Ext.extend(Ext.data.Store, {

treeNodePath :'',

getTreeNodePath : function(id){
this.treeNodePath = '';

this.makeTreeNodePath(id); // Your custom path generating method goes here

if (this.treeNodePath != '') this.treeNodePath = '/' + this.treeNodePath + id;
return this.treeNodePath;
},
makeTreeNodePath : function(id){
// Work back up the task record's parents and make a path string for the tree to use

// Get the parent of this task
var rec = this.conn.query("SELECT parentTaskId FROM task where taskId = '"+id+"'");
// If the task exists and there was a parent (ie not at the tree root), keep recursing
if (rec.length > 0) {
var theParentTaskId = rec[0].parentTaskId;
this.treeNodePath = theParentTaskId + '/' + this.treeNodePath;

// Recurse this function to repeat for the next node up the task list / node
this.makeTreeNodePath(theParentTaskId);

}
}
});

The code above is for a store based on a sqLite db. However, you could replace the content of the makeTreeNodePath() method with whatever you need to do to get a path string eg make a call to the server and get the path string returned back if you are using a jsonStore (for example)

Then, to use the extension, just do:


if (myTree.expandPathById(nodeId)) {
// do something with the node since it is now rendered and available
var aNode = myTree.getNodeById(nodeId);
aNode.select();
// etc ...
} else {
// there was a problem getting the path so there is no node rendered so take some other action
}


Cheers,
Murray

jesusangel
3 Oct 2009, 2:20 PM
Hi!

Thanks for your code. I'm trying to use it but it doesn't work for me:

The following line:

var path = this.getLoader().store.getTreeNodePath(id);

Throws this error:

this.getLoader().store is undefined

I have loaded this scripts:
ext-3.0.0/adapter/ext/ext-base-debug.js


ext-3.0.0/ext-all-debug.js


And this inline code


Ext.override(Ext.tree.TreePanel, {
expandPathById : function(id) {
// Take the node id passed in, make a path from the data in the store, then expand that path
// See if the node is already rendered. If so, just expand it

var node = this.getNodeById(id);

if (node) {
var path = node.getPath();
this.expandPath(path);
return true;
} else {
// The node with that id isnt rendered yet. Get a path to the node using the data from the store
var path = this.getLoader().store.getTreeNodePath(id); // New method. see below

if (path != '') {
// Expand that path
this.expandPath(path);

return true;

} else {
// Couldnt make a path so return false

return false;
}
}
}
});


Ext.data.ProjectNodeStore = Ext.extend(Ext.data.Store, {
treeNodePath :'',

getTreeNodePath : function(id) {
this.treeNodePath = '';
this.makeTreeNodePath(id); // Your custom path generating method goes here

if (this.treeNodePath != '') this.treeNodePath = '/' + this.treeNodePath + id;

return this.treeNodePath;

},

makeTreeNodePath : function(id) {
// Work back up the task record's parents and make a path string for the tree to use
// Get the parent of this task

//var rec = this.conn.query("SELECT parentTaskId FROM task where taskId = '"+id+"'");
var rec = 0;
this.treeNodePath = '1/24/44/26/'; // Just for testing. It's a valid path in my tree

// If the task exists and there was a parent (ie not at the tree root), keep recursing
if (rec.length > 0) {
var theParentTaskId = rec[0].parentTaskId;

this.treeNodePath = theParentTaskId + '/' + this.treeNodePath;

// Recurse this function to repeat for the next node up the task list / node
this.makeTreeNodePath(theParentTaskId);
}
}

});





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

Ext.onReady(function(){

var getnodesUrl = '/categories/getnodes/';
var reorderUrl = '/categories/reorder/';
var reparentUrl = '/categories/reparent/';

var Tree = Ext.tree;

var tree = new Tree.TreePanel({
el:'categories-tree-div',
border:false,
autoScroll:true,
animate:true,
enableDD:true,
containerScroll: true,
rootVisible: true,
loader: new Ext.tree.TreeLoader({
preloadChildren:true,
dataUrl:getnodesUrl
})
});

var root = new Tree.AsyncTreeNode({
text:'Recursos',
draggable:false,
id:'1',
expand: function(thisNode) {
alert(thisNode);
}
});
root.on('expand', function() {
//tree.expandPath(tree.getNodeById(25).getPath());
tree.expandPathById(26);
});
tree.setRootNode(root);

tree.render();
root.expand();
});



Could you please help me to fix the error?

Thanks

murrah
3 Oct 2009, 3:40 PM
Hi,

The app I am developing is an AIR app and has a custom treeLoader that pulls data from a local sqLite database into a store then to the treeLoader that populates the tree.

The standard treeLoader doesnt have a store. My comment wasnt as clear as it could be:
"The following works for an AIR app that has a tree store based on a sqLite db. However, it could easily be adapted for other types of tree stores/loaders."

I gather you are pulling the node data from a server. Since the standard treeLoader doesnt seem to have a store, instead of using the store extention I had in my post, you would need to make a call to the server to get the a path string back.

So

var path = this.getLoader().store.getTreeNodePath(id);
would need to be replaced with call to the server.

As it turns out, my app now has a tree populated with a standard treeLoader as well pulling data from a server. I am working on that right now and will need to modify my extention to work in that situation. As soon as I have it working (next day or so) I will post my solution.

By the way, I notice that your treeLoader.dataURL is '/categories/getnodes/' rather than a full web URL. Can you enlighten me about how that works please? Is there more code than you have shown? ie where is the data coming from?

Thanks,
Murray

murrah
4 Oct 2009, 4:00 PM
Ok. Here is what I did to make the extention work with a treeLoader that doesnt have a store ie the standard Ext.tree.TreeLoader.

Since the original idea was to make an extention to the TreePanel, this is one of those occassions when the Ajax call must be Synchronous - ie we need to wait for the server to return data before we can continue. This has drawbacks in some situations and you may want to reorganise your code so that you can use the standard Async response (although it would be tricky to make the code an extention of TreePanel in that case, I think).

I use the great ext-basex library by Doug Hendricks found here: http://www.extjs.com/forum/showthread.php?t=21681

Amongst other useful things, that adds the option to make the server call Synchronous.

So replace this line:
var path = this.getLoader().store.getTreeNodePath(id);

with this block:
var path = '';

Ext.Ajax.request({
url: 'http://www.myWebSite.com.au/mywebservice.cfc',
params: {
method: 'getNodePath',
Id: id,
reply: false
},
async: false,
method: 'POST',

success: function (result, request) {
path = result.responseText;
},

failure: function (result, request) {
Ext.MessageBox.alert('Error', 'There was an error: ' + result.statusText);
}
});

My real webservice is a ColdFusion server so the Ajax request is set up for that. You would need to change that to suit your situation.

As a final thought, since I need this functionality for both a local sqLite db and a server db, I have merged both options into the one extention. This might not suit you.

ie
var store = this.getLoader().store;
if (store) {
var path = this.getLoader().store.getTreeNodePath(id);
} else {
var path = '';
Ext.Ajax.request({
url: 'http://www.myWebSite.com.au/mywebservice.cfc',

etc...
}
Cheers,
Murray

jesusangel
4 Oct 2009, 11:40 PM
By the way, I notice that your treeLoader.dataURL is '/categories/getnodes/' rather than a full web URL. Can you enlighten me about how that works please? Is there more code than you have shown? ie where is the data coming from?

Hi

'/categories/getnodes/' is a relative URL:

If you have a tree in a web page with a URL like this:
http://www.mywebsite.com/page_with_tree.html

'/categories/getnodes' -> http://www.mywebsite.com/categories/getnodes

I'm using CakePHP, so http://www.mywebsite.com/categories/getnodes is a PHP script that returns a JSON object with the nodes requested

If you move your site to another domain like www.anotherwebsite.com (http://www.anotherwebsite.com), the code works:

'/categories/getnodes' -> http://www.anotherwebsite.com/categories/getnodes

murrah
5 Oct 2009, 1:10 AM
Oh yes, I see.

Since I have had my head in Adobe AIR for months (no, not an "air-head" ;) ) I lost sight of the web browser perspective. Better get away from this laptop for a while, I reckon.:-?

Thanks,
Murray

Sujan
24 Nov 2009, 7:57 AM
So here is my take on the problem - without the need to do additional requests or anything:


function buildNodePath(id, children, ids, paths, path) {

if(path == undefined)
var path = "";

if(ids.in_array(id)) {
paths.push(path);
}

path = path + "/" + id;

for(var i = 0, len = children.length; i < len; i++){
for(var i = 0, len = children.length; i < len; i++){
var child = children[i];
if(typeof(child.children) != "undefined")
var subchildren = child.children;
else if (typeof(child.attributes) != "undefined" && typeof(child.attributes.children) != "undefined")
var subchildren = child.attributes.children;
else
var subchildren = [];
buildNodePath(children[i].id, subchildren, ids, paths, path);
}

}and usage:


if(ids.length != 0) {

var o = tree.getNodeById('rootnode');
var paths = [];
buildNodePath('rootnode', o.childNodes, ids, paths)

for(var i = 0, len = paths.length; i < len; i++){
tree.expandPath(paths[i]);
}

}Here we have a tree in the variable tree, a rootnode with the id rootnode and an array ids with ids whose path we want to know.

If ids isn't empty we get the rootnode object into o and build an empty array paths. we call the function and tell it to start with the id "rootnode" on the childNodes of the root. We tell the function the ids and give it the empty array to store the results.

buildNodePath starts to build the path string and pushes the path-string into the result-array if the current id is in the array of ids we are searching for. it also adds the current id to the path and then loops over all the children of the current node. It gets the children of the currently processed children from one of 2 valid places and then calls itself.

So the function builds lots of path string and saves the ones that match our id array.

The magic here is to use node.attributes.children where the unrendered nodes are saved. So this only works if your initial ajax treeloader request delivered all nodes of the tree.

- Jan

murrah
24 Nov 2009, 12:02 PM
The magic here is to use node.attributes.children where the unrendered nodes are saved. So this only works if your initial ajax treeloader request delivered all nodes of the tree.

- Jan

Thanks Jan. Just to clarify, in my case the nodes are not all loaded initially so I needed to go back to the server to get them. Hence the method I outlined initially. Your case is another possibility - data in tree but just not rendered.

Cheers,
Murray