How To Use ExtJS 4 TreePanels with Both Static Data and Model Stores

by CLINT on SEPTEMBER 26, 2011




Wireframe showing tree with both static and dynamically-loaded items



I recently needed to build an “admin” screen for a webapp using ExtJS 4. The wireframe called for a tree to navigate the various admin settings, including settings for individual users (something like what you see at right). To build a tree like this with ExtJS, you usually use the Ext.tree.Panel class (a.k.a., a TreePanel). Unfortunately I couldn’t find any examples of how to make a TreePanel with a mixture of both static and dynamic nodes, so figuring it out took longer than it should have. With that in mind, I thought I’d share what I learned.

A working example of the solution I came up with is shown below (if the embedded version doesn’t appear for some reason, you can open it here). Note that you can actually run the code in your browser and see the results–just click the play button. Or if you’d like to modify it, click the “+” button to copy the code into your own project on JSFiddle.

 

The first thing the code does is define a basic model class for user data, called UserModel:


1. Ext.define('demo.UserModel', {  
2. 'Ext.data.Model',  
3. 'id', 'name', 'profile_image_url']  
4. });


The next thing it does is set up a TreeStore to load the UserModel instances:

1. var userTreeStore = Ext.create('Ext.data.TreeStore', {  
2.   
3. 'demo.UserModel',  
4.   
5.     proxy: {  
6. 'jsonp', // Because it's a cross-domain request  
7.   
8. 'https://api.twitter.com/1/lists/members.json?owner_screen_name=Sencha&slug=sencha-team&skip_status=true',  
9.   
10.         reader: {  
11. 'json',  
12. 'users' // The returned JSON will have array   
13. // of users under a "users" property  
14.         },  
15.   
16. // Don't want proxy to include these params in request  
17.         pageParam: undefined,  
18.         startParam: undefined,  
19.         pageParam: undefined,  
20.         pageParam: undefined  
21.     },  
22.     ...  
23. });


Note that for this example we’ll be using Twitter as our data source for faux “users”; the UserModel fields are set up to match JSON from the Twitter API (specifically, people who are a member of the “Sencha Team” Twitter list) and we need to use a JSONP proxy to make cross-domain requests (i.e., the demo is hosted at jsfiddle.net but it’s connecting to api.twitter.com). Here’s an easy-to-read sample of what the data looks like:



The JSON user data we're getting from Twitter (edited for easier reading)


The next part requires a little more explanation. First, let’s look at the code:

1. var userTreeStore = Ext.create('Ext.data.TreeStore', {  
2. 'demo.UserModel',  
3.     ...      
4.     listeners: {  
5.           
6. // Each demo.UserModel instance will be automatically   
7. // decorated with methods/properties of Ext.data.NodeInterface   
8. // (i.e., a "node"). Whenever a UserModel node is appended  
9. // to the tree, this TreeStore will fire an "append" event.  
10. function( thisNode, newChildNode, index, eOpts ) {  
11.               
12. // If the node that's being appended isn't a root node, then we can   
13. // assume it's one of our UserModel instances that's been "dressed   
14. // up" as a node  
15. if( !newChildNode.isRoot() ) {  
16.                   
17. // The node is a UserModel instance with NodeInterface  
18. // properties and methods added. We want to customize those   
19. // node properties  to control how it appears in the TreePanel.  
20.                   
21. // A user "item" shouldn't be expandable in the tree  
22. 'leaf', true);  
23.                   
24. // Use the model's "name" value as the text for each tree item  
25. 'text', newChildNode.get('name'));  
26.                   
27. // Use the model's profile url as the icon for each tree item  
28. 'icon', newChildNode.get('profile_image_url'));  
29. 'cls', 'demo-userNode');  
30. 'iconCls', 'demo-userNodeIcon');  
31.             }  
32.         }  
33.     }  
34. });  
35.   
36. userTreeStore.setRootNode({  
37. 'Users',  
38. false,  
39. false // If this were true, the store would load itself   
40. // immediately; we do NOT want that to happen  
41. });

This doesn’t look like a “normal” Ext.data.Store. For one thing, it has an “append” event handler that receives “nodes”–objects that have methods and properties from the Ext.data.NodeInterface class. Secondly, the store has a setRootNode()The important thing to understand here is that a TreeStore manages Ext.data.Model instances–just like any other Store–but it copies NodeInterface methods/properties into every model so that they can be linked together into a hierarchy (i.e., a tree). In this case, every instance of demo.UserModelNext, understand that when we call setRootNode({...}), the TreeStore implicitly creates a generic Ext.data.Model instance for us, adds the NodeInterface method/properties, and then makes it the root node; when the UserModels are loaded, they will be added as “children” to this node. What we end up with is a TreeStore with models organized into a hierarchy, each one having properties that a TreePanel can use for displaying it:


The "userTreeStore" builds a data structure like this



The next thing the code does is create a separate TreeStore with some “static” nodes (again, using NodeInterface config properties).


1. var settingsTreeStore = Ext.create('Ext.data.TreeStore', {  
2.     root: {  
3. true,  
4.         children: [  
5.             {  
6. 'Settings',  
7. false,  
8. true,  
9.                 children: [  
10.                     {  
11. 'System Settings',  
12. true  
13.                     },  
14.                     {  
15. 'Appearance',  
16. true  
17.                     }   
18.                 ]  
19.             }  
20.         ]  
21.     }  
22. });


Finally, we get the root of our userTreeStore and append it as a child to the “static” TreeStore. Then it’s just a matter of creating the TreePanel.

1. // Graft our userTreeStore into the settingsTreeStore. Note that the call  
2. // to .expand() is what triggers the userTreeStore to load its data.  
3. settingsTreeStore.getRootNode().appendChild(userTreeStore.getRootNode()).expand();  
4.   
5. Ext.create('Ext.tree.Panel', {  
6. 'usersTreePanel',  
7. 'Admin Control Panel',  
8.     renderTo: Ext.getBody(),  
9.     height: 300,  
10.     width: 300,  
11.     store: settingsTreeStore,  
12. false  
13. });


In other words, you can “graft” TreeStores onto other TreeStores–this is the key to creating tree panels with a mixture of different data sources and underlying Models. The prerequisite, however, is understanding the NodeInterface. Hopefully this post will help others learn how to get started with TreePanels and TreeStores more quickly.