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:

Ext.define('demo.UserModel', {
    extend: 'Ext.data.Model',
    fields: ['id', 'name', 'profile_image_url']
});

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

var userTreeStore = Ext.create('Ext.data.TreeStore', {

    model: 'demo.UserModel',

    proxy: {
        type: 'jsonp', // Because it's a cross-domain request

        url : 'https://api.twitter.com/1/lists/members.json?owner_screen_name=Sencha&slug=sencha-team&skip_status=true',

        reader: {
            type: 'json',
            root: 'users' // The returned JSON will have array 
                          // of users under a "users" property
        },

        // Don't want proxy to include these params in request
        pageParam: undefined,
        startParam: undefined,
        pageParam: undefined,
        pageParam: undefined
    },
    ...
});

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:

var userTreeStore = Ext.create('Ext.data.TreeStore', {
    model: 'demo.UserModel',
    ...    
    listeners: {
        
        // Each demo.UserModel instance will be automatically 
        // decorated with methods/properties of Ext.data.NodeInterface 
        // (i.e., a "node"). Whenever a UserModel node is appended
        // to the tree, this TreeStore will fire an "append" event.
        append: function( thisNode, newChildNode, index, eOpts ) {
            
            // If the node that's being appended isn't a root node, then we can 
            // assume it's one of our UserModel instances that's been "dressed 
            // up" as a node
            if( !newChildNode.isRoot() ) {
                
                // The node is a UserModel instance with NodeInterface
                // properties and methods added. We want to customize those 
                // node properties  to control how it appears in the TreePanel.
                
                // A user "item" shouldn't be expandable in the tree
                newChildNode.set('leaf', true);
                
                // Use the model's "name" value as the text for each tree item
                newChildNode.set('text', newChildNode.get('name'));
                
                // Use the model's profile url as the icon for each tree item
                newChildNode.set('icon', newChildNode.get('profile_image_url'));
                newChildNode.set('cls', 'demo-userNode');
                newChildNode.set('iconCls', 'demo-userNodeIcon');
            }
        }
    }
});

userTreeStore.setRootNode({
    text: 'Users',
    leaf: false,
    expanded: false // If this were true, the store would load itself 
                    // immediately; we do NOT want that to happen
});

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() method that we’re calling with a config object for NodeInterface. What’s going on?

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.UserModel now has NodeInterface properties like “leaf” and “text” (which indicate if the item should be expandable when it’s displayed in a TreePanel, and what text should be shown).

Next, 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).

var settingsTreeStore = Ext.create('Ext.data.TreeStore', {
    root: {
        expanded: true,
        children: [
            {
                text: 'Settings',
                leaf: false,
                expanded: true,
                children: [
                    {
                        text: 'System Settings',
                        leaf: true
                    },
                    {
                        text: 'Appearance',
                        leaf: true
                    } 
                ]
            }
        ]
    }
});

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.

// Graft our userTreeStore into the settingsTreeStore. Note that the call
// to .expand() is what triggers the userTreeStore to load its data.
settingsTreeStore.getRootNode().appendChild(userTreeStore.getRootNode()).expand();

Ext.create('Ext.tree.Panel', {
    id: 'usersTreePanel',
    title: 'Admin Control Panel',
    renderTo: Ext.getBody(),
    height: 300,
    width: 300,
    store: settingsTreeStore,
    rootVisible: false
});

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.

Clint Harris is an independent software consultant living in Brooklyn, New York. He can be contacted directly at ten.sirrahtnilc@tnilc.
  • http://erikras.com/?utm_source=disqus&utm_medium=profile&utm_campaign=Disqus%2BProfile Erik R.

    Very interesting. I’d never have thought to try a graft.

    I found this post while looking for the solution to my current problem: I’ve set autoSync on my TreeStore, which is set up as “rest” just like my working regular Stores, but calling store.getRootNode().appendChild({someData:’here’}) isn’t sending off any ajax requests. Any ideas?

  • Anonymous

    Hmm…not sure about that. I haven’t tried using TreeStores to do two-way syncing. Try calling yourtreestore.getNewRecords() to make sure it’s actually returning the model for the new node you’ve appended. Also, try manually calling the .sync() method on your store. If all else fails, I’d go through the source for TreeStore and fire up the debugger…

  • http://erikras.com/?utm_source=disqus&utm_medium=profile&utm_campaign=Disqus%2BProfile Erik R.

    Thanks, yeah. I’m deep in debugger hell. Somehow my root note that I’m appending to thinks its store is a NodeStore, which isn’t the instance I set autoSync on. Perhaps it’s a consequence of how ExtJs is doing its NodeInterface mixin.

    Back to debugging… If I discover anything particularly interesting, I might post back here. Cheers!

  • http://erikras.com/?utm_source=disqus&utm_medium=profile&utm_campaign=Disqus%2BProfile Erik R.

    Aha! A known bug.
    http://www.sencha.com/forum/showthread.php?136172-TreeStore-autoSync-Isn-t-Passed-Along-to-NodeStore

  • Ilya Fadeev

    Thank you for a nice post! This is exactly about what I am working on right now: trying to use different models for one treepanel.
    As I understood from official docs and forums:

    “Tree’s with multiple models is not something we will be supporting for 4.0. For now every TreeStore is based on one model. This should be sufficient for most Tree use cases and for the special cases there are workarounds.”
    (http://www.sencha.com/forum/archive/index.php/t-130060.html?s=2e3bd8b97cfb147c80cc805d8766980e)

    So, your solution looks like a “workaround” meant by smbd from sencha dev team (if I am not mistaken).

    But I can see a problem:

    if in your example I try to collapse “Users” node and then expand it I have the following behaviour:
    - the store will load more data and add duplicates children.
    After this “Users” cannot be collapsed.

    The error in debug is this one:
    “TypeError: Cannot read property ‘internalId’ of undefined [http://fiddle.jshell.net/js/lib/ext-4.0.2a/ext-all.js:15]”

    I think this way we are breaking the main treestore’s node we are appending our new child.

    If you have an idea how to make your solution work properly I would be very glad if you share.

    Best regards,
    Ilya

  • https://clintharris.net Clint Harris

    Hi Ilya. You are absolutely correct–I’m shocked that I didn’t notice that issue. Unfortunately I don’t have time to dig in to solving this right now, but I will have to in the next week or two. I will share whatever I find then (or email you to see if you have a solution). ;)

  • http://www.facebook.com/people/Salvador-Ulloa/100001237532992 Salvador Ulloa

    Excellent!!!!!
    Thank you very much for your time, I’ve been looking for this, but could not find anything worthwhile. Congratulations for being so didactic and hope to find more tutorials like this. Greetings.

  • http://twitter.com/purobox Puro Box

    Hi,
    I’m having some problem.
    IF I Extend the Ext.data.TreeStore, the model property is ignored, so I cannot use my own Model.

    Is this a bug ? I’m using Ext 4.0.7

  • http://www.facebook.com/profile.php?id=100001794636678 Robert Ritschel

    Hello,

    there is already a solution for the problem:
    - the store will load more data and add duplicates children.
    After this “Users” cannot be collapsed.

  • http://www.facebook.com/people/??????-??????/100000075984298 ?????? ??????

    I think this:
    settingsTreeStore.getRootNode().appendChild(userTreeStore.getRootNode()).expand();
    is not very good solution. Especially when loading userTreeStore takes much time (5 to 7 secs)

    I suggest you to use event load for appendChild. e.g.
    userTreeStore.on(‘load’,function(store,node,records){
    this.store.getRootNode().appendChild(store.getRootNode());
    },settingsTreeStore);

    Here is my post on Sencha forum dedicated to this problem: http://www.sencha.com/forum/showthread.php?174996-TreePanel-with-different-models

  • Anonymous

    hi gan.
    I have one grid and two panels form..
    in here I will be loading this grid data from i have panels form together..how I do it’s
    please give me solutions for this my problem it..

  • http://www.facebook.com/profile.php?id=574662601 Mohit Khanna

    @Ilya

    Yes. I tried the above logic and facing the exact same issue you mentioned (‘internalId’ of undefined).

    Any idea how to get this fixed? So exactly what are we doing wrong in the above logic?

    @Clint

    You have any idea?

  • msj2004

    Hi!

    What an informative article on building treepanels!

    Can I ask (if possible) for the sample code of this demo app? I’d like to extend it so that it now includes the CRUD functionality. We needed it for our next project.

    Many thanks in advance for the time and help! =)

  • http://mike.post.myopenid.com/ Guest

    Thanks, this was a really informative post that steered me in the right direction. However, it looks like there’s an issue with using more than one model: any nodes that are not of the model type declared for the store will lack expand/collapse widgets when displayed in a tree panel. This situation is not evident from your example because your tree only has two 2 levels (after the root). Once you go to 3 or more levels (where you would have two or more expandable levels in your hierarchy), this problem starts showing up.

  • Diego Dias

    Excellent, but I’m having trouble using the data from my database that are also pulled by a “url”.
    Do not know if it’s the type of “proxy”, “reader” or “root” or Dunno!!
    If someone can help me. The error that appears is:

    SyntaxError: invalid label

  • Sneha Rathod

    Does treegrid support paging toolbar??? I am trying to add one as dockedtoolbar like examples shown on internet . But the toolbar does not work. I get errors like “Uncaught TypeError: undefined is not a function ext-all.js:21Ext.cmd.derive.getPageData”. Console log show it at “,getPageData:function(){var b=this.store,a=b.getTotalCount(); “in ext-all.js file. Do you have any idea on this error wrt to TREEGRID in EXT JS.