Ember JS: Dynamic Tree View

A tree consists of nodes and branches. Each branch can contain more nodes and/or branches. The branches can be represented by nested
ul tags and the nodes by li tags within them. We will create 2 Ember views, one for the nodes and the other for the branches. An Ember view has a tagName property which will be assigned to the html tag it creates. We will use an Ember CollectionView to represent the branches. A collection view renders its content within the itemViewClass you specify. Within our TreeBranchView we will use a TreeNodeView to render the content

App.TreeBranchView = Ember.CollectionView.extend({ 
    tagName: 'ul',
    content: [{'name': 'Paul', 'age': 10, 'branch': true},{'name': 'Tom','age': 5, 'branch': false},{'name': 'Paul', 'age': 7, 'branch': false}], 
    classNames: ['treebranch'],
    itemViewClass: 'App.TreeNodeView'
});

You can see that the content has been added to the view but you could load it from a controller if you wanted.

In order to show a different icon depending on whether the branch is open or closed or if it is a node we will use css and the Ember view property classNameBindings. A classNameBindingtells ember to either always assign the css class name or to assign it depending on another property of the view. We will use the class names ‘opened’ and ‘branch’ and specify them as

classNameBindings: ['opened: tree-branch-open', 'branch:tree-branch-icon:tree-node-icon']

The class tree-branch-open will be added to the view and therefore the li tag that represents it if the view property opened is true. Otherwise the class name tree-branch-open will not be applied to the view. The class name tree-branch-icon will be applied to the view if the property branch is true otherwise the class name tree-node-icon will be applied to it. Remember that this all happens dynamically, you do not have to do anything programatically.

So that we can open and close branches and add more data we will listen to the click handler for the node. If the item clicked on is a branch then first time round we will create a new TreeBranchView programatically within the click handler, dynamically insert it as a sub view under this branch and then save this sub view as a property of the parent view. If the branch has already been opened, ie the property opened is true, then we will remove the sub views (remember that we saved the sub view as a property of the parent). Ember will handle all the rendering for us.

We also need a template for the node view. In the code below you can see that the template is specified within the class, this is due to an issue I was having with ember finding the template within the html when inserting the view dynamically. Maybe it will have been fixed by now or maybe I was not doing it correctly? Anyway specifying the template in the view works just fine. In this example the template is not very exciting and shows the same whether it is a node or a branch, you may want to use some logic to show the data appropriate to your app.

App.TreeNodeView = Ember.View.extend({ 
    opened: false,
    branch: function(){
        return this.get('content').branch;
    }.property(),
    subBranch: undefined,
    fetchedData: false,
    tagName: 'li',
    // class names that determine what icons are used beside the node
    classNameBindings: ['opened: tree-branch-open', 'branch:tree-branch-icon:tree-node-icon'], //templateName: 'treenode',
    // Ember had some issues with finding the treenode template when the branch view is dynamically added to
   // the parent collection view in the click event. Had to compile the template here instead
   template: Ember.Handlebars.compile('{{view.content.name}} {{view.content.age}}'),
   click: function (evt) {
       if (this.get('opened')) {
           // user wants to close the branch
           var index = this.get('parentView').indexOf(this) + 1;
           this.get('parentView').removeAt(index);
           this.set('opened', false);
       } else if (this.get('fetchedData')) {
           // user wants to open the branch and we have already created the view before
           var index = this.get('parentView').indexOf(this) + 1;
           this.get('parentView').insertAt(index, this.get('subBranch'));
           this.set('opened', true);
       } else if (this.get('branch')) {
           // user wants to open the branch for the first time
           var name, age; 
           var me = this;
           name = this.get('content').name;
           age = this.get('content').age;
           var treeBranchView = App.TreeBranchView.create();
           treeBranchView.set('content', [{ 'name': 'John', 'age': 10, 'branch': true }, { 'name': 'Tom', 'age': 5, 'branch': false }, { 'name': 'Paul', 'age': 7, 'branch': true }]);
           var index = me.get('parentView').indexOf(me) + 1;
           me.get('parentView').insertAt(index, treeBranchView);
           me.set('opened', true);
           me.set('subBranch', treeBranchView);
           me.set('fetchedData', true); 
       } 
   }
});

Don’t forget that you need a handlebars template in the html for your initial TreeBranchView, {{view App.TreeBranchView}}

Here is a JSfiddle with all the code plus the CSS used for the icons.