PDA

View Full Version : Combining TreeStores (same model) into one TreeStore for single tree.Panel



myExtJsUname
22 Oct 2012, 1:46 PM
Hello all,

I have multiple services whose response models are the same and I have no problem creating a TreeStore for any one of the services and using that store to populate the TreePanel. What I can't figure out is how to combine those TreeStores into a single TreeStore and use that to populate the tree.Panel. It's trying to do it, but the results are whacky.

A little background...

The JSON returned by the services is not in the correct format for an ExtJs TreeStore (and I can't edit the services). However, the response contains all the info I need to build a JSON that is correct for the tree. So, (with some pointers from vietits) I've created my own reader that essentially returns the following JSON from its readRecords function:


readRecords: function(data){
return [
{
name: data.documentInfo.Title // e.g., "MyService1"
,children: [
{
name: data.layers[0].name // e.g., "MyService1 Child1"
,layerId: data.layers[0].id // e.g., 0
}
,{
name: data.layers[1].name "MyService1 Child2"
,layerId: data.layers[1].id // e.g., 1
}

]
}
];
}


And, calling a different service would return:


[
{
name: "MyService2"
,children: [
{
name: "MyService2 Child1"
,layerId: 0
}
]
}
]


The code to do the combining (and I haven't included everything because, like I said, there are no problems with creating a single TreeStore and using it to populate the tree.Panel, just the combining of the two) is essentially:


var treeStore1 = Ext.create(
"LayersTreeStore"
,{
url: "http://<MyDomain>/<MyService1>?f=pjson"
,root: {
name: "MyRoot"
}
}
);
var treeStore2 = Ext.create(
"LayersTreeStore"
,{
url: "http://<MyDomain>/<MyService2>?f=pjson"
,root: {
name: "LayersTreeStoreVar2"
}
}
);
treeStore1.getRootNode().appendChild(treeStore2.getRootNode());

this.pnlTree = Ext.create(
"Ext.tree.Panel"
,{
store: treeStore1
,rootVisible: true // will become false when I get it to work
,hideHeaders: true
,columns: [
{
xtype: 'treecolumn',
flex: 1,
dataIndex: 'name'
}
]
}
);


What I would like to have appear in the tree.Panel is something like:


- MyRoot // which will ultimately be hidden (rootVisible = false in tree.Panel)
- MyService1
- MyService1 Child1
- MyService1 Child2
- MyService2
- MyService2 Child1


However, what I get is some whacky combination of the two:


- MyRoot
- MyService2
- MyService1 Child1
- MyService1 Child2
- MyService2 Child1


I think I need to create an empty TreeStore (the one that will actually have the real "MyRoot" that will be hidden in the tree.Panel) and then append the first child from each subsequent TreeStore into it. The problem is that I can't figure out how to append the child node, because the contents of the TreeStores haven't been loaded yet.

I've tried loading them and appending them as in:


this.treeStore = Ext.create(
"Ext.data.TreeStore"
,{
root: "MyRoot"
}
);

var treeStore1 = Ext.create(
"LayersTreeStore"
,{
url: "http://<MyDomain>/<MyService1>?f=pjson"
,root: {
name: "LayersTreeStore1"
}
}
);
treeStore1.load();
this.treeStore.getRootNode().appendChild(treeStore1.getRootNode());

var treeStore2 = Ext.create(
"LayersTreeStore"
,{
url: "http://<MyDomain>/<MyService2>?f=pjson"
,root: {
name: "LayersTreeStore2"
}
}
);
treeStore2.load();
this.treeStore.getRootNode().appendChild(treeStore2.getRootNode());


But when I try to expand them, they just whirl away into never-never land. And only one of the Stores every really tries to get loaded.

I've tried a bunch of different things to figure this combining thing out (other approaches just fail with a "node is undefined" message in the Ext.data.NodeInterface appendChild or createNode functions) and thought it was time to stop spinning my wheels and ask if anyone knows the proper way to go about doing this (or more importantly that it can be done) before I continued going around in circles.

Any pointers to info or references to documentation that might help with this are appreciated.

Cheers
jtm

vietits
22 Oct 2012, 5:08 PM
Try this:


Ext.onReady(function(){
Ext.define('TreeNode', {
extend: 'Ext.data.Model',
fields: ['text', 'leaf', 'children']
});

var treeStore = Ext.create('Ext.data.TreeStore', {
model: 'TreeNode',
root: {
text: 'Root'
},
appendTree: function(tree){
function appendNode(sNode, dNode){
if(sNode && dNode){
var nNode = sNode.appendChild(dNode.raw);
if(dNode.hasChildNodes){
var childNodes = dNode.childNodes;
for(var idx = 0, len = childNodes.length; idx < len; idx++){
appendNode(nNode, childNodes[idx]);
}
}
}
}
appendNode(this.getRootNode(), tree.getRootNode());
}
});


Ext.create('Ext.data.TreeStore', {
model: 'TreeNode',
root: {
text: 'Root 1'
},
autoLoad: true,
proxy: {
type: 'ajax',
url: 'tree1.json',
reader: {
type: 'json'
}
},
listeners: {
load: function(){
treeStore.appendTree(this);
}
}
});


Ext.create('Ext.data.TreeStore', {
model: 'TreeNode',
root: {
text: 'Root 2'
},
autoLoad: true,
proxy: {
type: 'ajax',
url: 'tree2.json',
reader: {
type: 'json'
}
},
listeners: {
load: function(){
treeStore.appendTree(this);
}
}
});


var tree = Ext.create('Ext.tree.Panel', {
renderTo: Ext.getBody(),
width: 400,
height: 200,
store: treeStore
});
});


tree1.json


{
"success": true,
"children": [{
"text": "Node 1.1",
"leaf": true
},{
"text": "Node 1.2",
"leaf": false,
"children": [{
"text": "Node 1.2.1",
"leaf": true
}]
}]
}

tree2.json


{
"success": true,
"children": [{
"text": "Node 2.1",
"leaf": false,
"children": [{
"text": "Node 2.1.1",
"leaf": true
}]
},{
"text": "Node 2.2",
"leaf": true
}]
}

myExtJsUname
26 Oct 2012, 12:40 PM
A very big "Thank you!" to vietits for the response to this and previous posts about the work I am trying to accomplish. It's taken a little while but, with that help, I've managed to get essentially what I'm looking for in my application.

I did, however, have a couple outstanding questions with respect to this combining of two TreeStores into one for display in a Tree panel that I'm hoping someone can comment on. Don't waste any time on them; just if you know something off the top of your head, I'd appreciate a note.

I am including a sample that examplifies what I am doing within my application. It uses some public services whose responses are similar to what I am working with. Obviously, you'll need to change your ExtJs paths at the top (I'm using 4.1.1). After loading the page, click on the button and a window will appear with the tree panel that includes two TreeStores.

<html>
<head>
<link rel="stylesheet" type="text/css" href="/ext-4.1.1/resources/css/ext-all.css">
<script type="text/javascript" src="/ext-4.1.1/ext-all-debug.js"></script>
<script type="text/javascript">

Ext.application(
{
name: "LayerPicker"
,launch: function(){
Ext.create(
"Ext.container.Viewport"
,{
layout: "fit"
,items: [
new Ext.Button(
{
text:'Open Window'
,handler: openWindow
}
)
]

}
);

Ext.define(
"LayersModel"
,{
extend: "Ext.data.Model"
,fields: [
{
name: "id"
,type: "int"
}
,{
name: "name"
,type: "string"
}
,{
name: "parentLayerId"
,type: "int"
}
,{
name: "defaultVisibility"
,type: "boolean"
}
,{
name: "subLayerIds"
,type: "string"
}
,{
name: "minScale"
,type: "float"
}
,{
name: "maxScale"
,type: "float"
}
]
}
);

Ext.define(
'LayersTreeStoreReader'
,{
extend: 'Ext.data.reader.Json'
,alias: "reader.layerstreestorereader"
,readRecords: function(data){
return this.callParent([this.convertData(data)]);
}
,convertData: function(data){
var treeData = [];
var service = {
name: data.documentInfo.Title
,id: data.documentInfo.Title
,children: []
};

function getChildren(childIdx){
var results = [];
if(childIdx){
for(var idx = 0, len = childIdx.length; idx < len; idx++){
var record = data.layers[childIdx[idx]];
if(record){
var children = getChildren(record.subLayerIds);
results.push({
id: record.id,
name: record.name,
children: children,
leaf: (children.length < 1)
});
}
}
}
return results;
}


for(var idx = 0, len = data.layers.length; idx < len; idx++){
var record = data.layers[idx];
if(record.parentLayerId == -1){
var children = getChildren(record.subLayerIds);
service.children.push({
id: record.id,
name: record.name,
children: children,
leaf: (children.length < 1)
});
}
}

treeData.push(service);
return treeData;
}
}
);

Ext.define(
"LayersTreeStore"
,{
extend: "Ext.data.TreeStore"
,uses: [
"LayersTreeStoreReader"
,"LayersModel"
]
,model: "LayersModel"

,constructor : function(config){
this.proxy.url = config.root.url + "?f=pjson";;

this.initConfig(config);
this.callParent([config]);
}

,proxy: {
type: "jsonp"
,reader: {
type: "layerstreestorereader"
}
}

,findByRawValue: function(node, attr, value) {
if (node == null){
return null;
}

var tmpNode;
var children = node.childNodes;
for(var i = 0, len = children.length; i < len; i++) {
if(children[i].raw[attr] === value){
return children[i];
}
else {
tmpNode = this.findByRawValue(children[i], attr, value);
if (tmpNode){
return tmpNode;
}
}
}
return null;
}
}
);
}
}
);

var treeStore = null;

function openWindow(){
Ext.define(
"TreeNode"
,{
extend: "Ext.data.Model"
,fields: ["name", "leaf", "children"]
}
);

var treeStore = Ext.create(
"Ext.data.TreeStore"
,{
model: "TreeNode"
,root: {
name: "Layers"
}
,appendTree: function(tree){
function appendNode(sNode, dNode){
if(sNode && dNode){
var nNode = sNode.appendChild(dNode.raw);
if(dNode.hasChildNodes){
var childNodes = dNode.childNodes;
for(var idx = 0, len = childNodes.length; idx < len; idx++){
appendNode(nNode, childNodes[idx]);
}
}
}
}

tree.getRootNode().childNodes[0].raw.url = tree.getRootNode().raw.url;

appendNode(this.getRootNode(), tree.getRootNode().childNodes[0]);
}
}
);

if (true){
var treeStore1 = Ext.create(
"LayersTreeStore"
,{
model: "TreeNode"
,autoLoad: true
,visibleLayers: [6]
,root: {
name: "Root 1"
,url: "http://servicesbeta2.esri.com/arcgis/rest/services/Military/MapServer"
}
,listeners: {
load: function(){
treeStore.appendTree(this);

var node;
for (var i = 0; i < treeStore.getRootNode().childNodes.length; i++){
node = treeStore.getRootNode().childNodes[i];
if (node.raw.url === this.getRootNode().raw.url){
break;
}
node = null;
}
if (node != null){
node = this.findByRawValue(node, "id", this.visibleLayers[0]);
if (node != null){
pnlTree.selectPath(node.getPath());
}
}
}
}
}
);
}

if (true){
var treeStore2 = Ext.create(
"LayersTreeStore"
,{
model: "TreeNode"
,autoLoad: true
,root: {
name: "Root 2"
,url: "http://servicesbeta2.esri.com/arcgis/rest/services/Census/MapServer"
}
,listeners: {
load: function(){
treeStore.appendTree(this);
}
}
}
);
}

var pnlTree = Ext.create(
"Ext.tree.Panel"
,{
store: treeStore
,rootVisible: true
,hideHeaders: true
,columns: [
{
xtype: 'treecolumn',
flex: 1,
dataIndex: 'name'
}
]
,listeners: {
itemclick: function(view, rec, item, index, eventObj) {
if (rec.isLeaf()){
var child = rec;
var parent = child.parentNode;
while (!parent.isRoot()){
child = parent;
parent = child.parentNode;
}
alert(child.raw.url + "/" + rec.raw.id)
}
}
}
}
);

var myWin = Ext.create(
'Ext.window.Window'
,{
title: 'Layer Picker'
,width: 300
,height: 300
,layout: "fit"
,items:[
pnlTree
]
}
);
myWin.show();
}
</script>

</head>
<body>
</body>
</html>

And, my questions...

I would prefer that the top level root ("Layers") from the encompassing treeStore be hidden in the tree.Panel itself (rootVisible = false). That doesn't seem to be possible (I can't seem to get around "url is undefined" messages for the root). Is it possible (in some way that I haven't been able to figure out thus far)? If not, it's not important. We can live with leaving "Layers" there.

There's also one thing I'm not so thrilled with in the appendTree function for the encompassing treeStore. There, I am moving the TreeStore's url parameter/value from the root node to the first child node, since that's what I'm actually appending to the encompassing tree. Instead, I'm wondering if there is any way to get access to that information from within the reader itself during the readRecords process, in the convertData when I am defining that first child node? That is, from within the reader, is there some way to get access to the store it is being used to populate? Again, I can live with what I've got. I'd just prefer to ensure all the necessary information for the nodes for a single service is assembled in one place.

Finally, I introduced a findByRawValue function in the TreeStore definition, but I'm wondering if there is a better way to do that. I came across some documentation that suggested there is a "findBy" method available for Stores, but that doesn't seem to be the case for a TreeStore. Is there a provided way to find a node in a tree when the only information I have is the value of a raw attribute associated with that node? This one I'm actually quite interested in, because I really can't believe other people wouldn't be doing this all the time, so I'm thinking I've just missed something in the documentation somewhere.

Cheers, and thanks again to vietits!
jtm

vietits
27 Oct 2012, 4:41 AM
I would prefer that the top level root ("Layers") from the encompassing treeStore be hidden in the tree.Panel itself (rootVisible = false). That doesn't seem to be possible (I can't seem to get around "url is undefined" messages for the root). Is it possible (in some way that I haven't been able to figure out thus far)? If not, it's not important. We can live with leaving "Layers" there.

Just config tree panel with rootVisible set to false and config proxy of the tree store to 'memory'.


var treeStore = Ext.create(
"Ext.data.TreeStore"
,{
model: "TreeNode"
,root: {
name: "Layers"
}
,proxy: 'memory'
,appendTree: function(tree){





There's also one thing I'm not so thrilled with in the appendTree function for the encompassing treeStore. There, I am moving the TreeStore's url parameter/value from the root node to the first child node, since that's what I'm actually appending to the encompassing tree. Instead, I'm wondering if there is any way to get access to that information from within the reader itself during the readRecords process, in the convertData when I am defining that first child node? That is, from within the reader, is there some way to get access to the store it is being used to populate? Again, I can live with what I've got. I'd just prefer to ensure all the necessary information for the nodes for a single service is assembled in one place.

If you want to access store or some values from reader, you can assign them to the reader in the store constructor.


Ext.define(
"LayersTreeStore"
,{
extend: "Ext.data.TreeStore"
,uses: [
"LayersTreeStoreReader"
,"LayersModel"
]
,model: "LayersModel"


,constructor : function(config){
this.proxy.url = config.root.url + "?f=pjson";
this.proxy.reader.store = this;
// this.proxy.reader.url = config.root.url;


this.initConfig(config);
this.callParent([config]);
}





Finally, I introduced a findByRawValue function in the TreeStore definition, but I'm wondering if there is a better way to do that. I came across some documentation that suggested there is a "findBy" method available for Stores, but that doesn't seem to be the case for a TreeStore. Is there a provided way to find a node in a tree when the only information I have is the value of a raw attribute associated with that node? This one I'm actually quite interested in, because I really can't believe other people wouldn't be doing this all the time, so I'm thinking I've just missed something in the documentation somewhere.

If your node has an id field, then you can use <treeStore>.getNodeById().

myExtJsUname
30 Oct 2012, 10:26 AM
To wrap this one up...

1. Setting the parent TreeStore's proxy to "memory" allows me to set rootVisible to "false" in the tree.Panel and lose the "Layers" node that was displaying before (as desired).

2. I'm now passing the value I want available in the reader through the store constructor as suggested. In my case, I'm just passing the url to the service (rather than the entire store) and setting it in the root within the convertData method. I've removed transfer of this information from the appendTree method. (And, I have to say I'm embarrassed that I didn't think of that because I do it all the time in other places. For whatever reason, I couldn't get out of my head the erroneous idea that I should already have access to the object the reader was reading into and I just needed to find it.)

3. In my case, the getNodeById will not work because the id of the node is not unique within the parent TreeStore (which is where I need to find it, not within the child TreeStore it was originally in). So, I can't use TreeStore.getNodeById() because I need to limit the search to a child node under the parent TreeStore's root before the id becomes unique. Anyway, I'm guessing, given that answer and other things I've read, that there isn't any existing way to search for a matching node when the only thing you have is a value that exists within an attribute in the raw array (this would probably be more clear if my particular example wasn't actually using id to search on). If I ever come across something that disproves that, I'll tack on another note with directions; or, if someone knows a better way to do it, please tell.

Anyway, things are all good. Yay! Thank you very much for your help, vietits!!

Cheers,
jtm