Back to top
« Back to Billy's Billing accounting software

Billy's Developer Blog

Tips & Tricks for Developers

How to Implement a Tree in Ember.js

Posted on Mar. 22nd 2013 by Sebastian Seilund, viewed 723 times
How to Implement a Tree in Ember.js
This post is for developers. Representing data structures such as trees is really simple with Ember.js. I'll show you how to do it in about 50 lines of code. We will use the experimental {{control}} Handlebars helper.

Trees are useful for a lot of stuff in web apps. Displaying filesystems, CMS page hierachy, product groups etc. But they can be troublesome to implement because of the nested data structure.

This is what we're going to build with surprisingly few lines of code:

Ember.js tree screenshot

If you want to see the finished result you can scroll to the bottom of the page for a live demo.

With Ember.js implementing a tree is a piece of cake. If you haven't had any experience with Ember yet, I suggest you check out their guides first.

Simple application skeleton

First let's setup the basic skeleton for our application together with a data representation of the tree.

App = Ember.Application.create();
 
//This is just an extract of the data we'll use
App.set('treeRoot', {
    text: 'Root',
    children: [
        {
            text: 'People',
            children: [
                {
                    text: 'Basketball players',
                    children: [
                        {
                            text: 'Lebron James'
                        },
                        {
                            text: 'Kobe Bryant'
                        }
                    ]
                }
            ]
        }
    ]
});

How you structure the data is up to you, as long as each node at least has a text string property and a children array of other nodes. You could also use a data framework such as Ember Data.

{{control}} helper

Before we continue, I'll quickly explain something about the {{control}} Handlebars helper that we're going to use. It's currently "under development and is considered experimental" (Ember API Docs), but it's very nifty for things like this.

The {{control}} helper is a very simple and elegant way to create a child view. It makes it easy to reuse components across your app, or split up large complex views into smaller more concise views.

The syntax of the control helper is:

{{control "name" model}}

Where name is a string that corresponds to a controller class and a view class, and model is a reference to an object that our view should be displaying, such as a tree node. If you used {{control "comment" comment}}, Ember would do the following:

  • Instantiate an instance of App.CommentController.
  • Set the comment object as the controller's content property.
  • Instantiate an instance of App.CommentView, which will be wired up with the controller.
  • The view will default to use a template named comment. You can override this by setting templateName in your view class definition.

Normally when you define a controller, Ember will set it up as a singleton. When you're using {{control}}s you do not want singleton controllers. So remember to define your controller like this:

App.CommentController = Ember.ObjectController.extend({
});
App.register('controller:comment', App.CommentController, {singleton: false});

Now you automatically (dare I say automagically) have a clean MVC structure. Your model can be augmented by the controller, and the view is only responsible for rendering HTML and delegating DOM events (if any) to the controller.

Since {{control}} is experimental (for now) you need to enabled it by setting ENV.EXPERIMENTAL_CONTROL_HELPER = true before you include the Ember source code.

Adding node and branch templates

A tree consists of nodes and branches. So let's add a Handlebars template for each one. Here is the tree-branch template:

<script data-template-name="tree-branch" type="text/x-handlebars">
    {{each children itemController="treeNode" itemViewClass="App.TreeNodeView"}}
</script>

The non-block version of the {{each}} helper works pretty much like {{control}}, except that it creates a view for each item in children and wires it up with a controller which get its content property set to a node.

Here is the tree-node template:

<script data-template-name="tree-node" type="text/x-handlebars">
    <span {{bindAttr class=":toggle-icon children.length::leaf"}} {{action toggle}}>
        {{#if isExpanded}}
            &#x25BC;
        {{else}}
            &#x25B6;
        {{/if}}
    </span>
    <a {{action click}} class="text">{{text}}</a>
    {{#if isExpanded}}
        {{control "treeBranch" content}}
    {{/if}}
</script>

Depending on isExpanded, which is a property on our App.TreeNodeController that we'll implement in a moment, we either display an arrow down or arrow right. That's what those weird HTML entities are. The template registers {{action}} handlers for clicking the toggle icon and the node text itself. To display a tree correctly each node should display it's own sub-branch, but only if the node is currently expanded. This way we get a simple recursive view.

Adding view and controller classes

First we create a controller and a view for branches:

App.TreeBranchController = Ember.ObjectController.extend({
});
App.register('controller:treeBranch', App.TreeBranchController, {singleton: false});
 
App.TreeBranchView = Ember.View.extend({
  tagName: 'ul',
  templateName: 'tree-branch',
  classNames: ['tree-branch']
});

Nothing really interesting is going on there. Let's move on to the nodes:

App.TreeNodeController = Ember.ObjectController.extend({
    isExpanded: false,
    toggle: function() {
        this.set('isExpanded', !this.get('isExpanded'));
    },
    click: function() {
        console.log('Clicked: '+this.get('text'));
    }
});
App.register('controller:treeNode', App.TreeNodeController, {singleton: false});
 
App.TreeNodeView = Ember.View.extend({
  tagName: 'li',
  templateName: 'tree-node',
  classNames: ['tree-node']
});

The App.TreeNodeController is more interesting. It tells its template that by default it's collapsed (isExpanded: false). It implements the two action handlers toggle and click that the template setup.

Adding the tree control to the application

Now we only have one thing left to do. Adding the tree to the application! It's easy. Since we don't want to display the root node we simply use a {{control}} for treeBranch with App.treeRoot as the model:

<script data-template-name="application" type="text/x-handlebars">
    <h1>Ember.js Tree Example</h1>
    {{control "treeBranch" App.treeRoot}}
</script>

Note that Ember automatically renders the application template in the <body> behind the scenes.

And that's all you need to make a simple tree structure in Ember.js. Isn't it astonishing how few lines of code we needed to write!? Here is the working example:

Ember.js Tree Example

If you want to see the complete source code, you can click the HTML and Javascript tabs.

You can click the arrows to expand/collapse each branch. And you can click the node text to console.log the node.

Then you say: "That's just a boring tree?"

"You're right, but the possibilities are endless", I would say. Want to have a checkbox next to each node and display a list of only the selected nodes? That's easy:

Ember.js Tree Example With Selection

All I had to do was:

  • Setup an array called App.selectedNodes.
  • Add a checkbox to the tree-node template.
  • Add an observer on checked in the App.TreeNodeController, which pushes/removes the controller's node to/from App.selectedNodes.

One shortcoming of this example is that currently {{control}} doesn't support other bindings than the model. It would be much cooler, if the application template could invoke the control like {{control "tree" App.treeRoot selectedNodesBinding="App.selectedNodes"}}. This way the tree would keep track of its own selectedNodes and it would thereby be possible to have multiple trees with each its own selection. Hopefully the Ember devs will add this feature really soon.

"That's not very dynamic? Show me some magic!"

Since this tree is implemented in Ember.js it benefits fully from the built-in template bindings. Let's say we want to be able to add items to the tree. That's easy too. Just click a node to append a child to it:

Ember.js Tree Example With Selection

All I had to do this time was:

  • Change the click {{action}} handler to addChild.
  • Implement addChild method on App.TreeNodeController, which prompts for a name for the child, and pushes it to the controller's node's children array.

If you weren't already aware, I hope this post makes you see just how awesome Ember.js is.

Update: Implementing trees with the new Ember.Component is even more elegant. See this updated example: http://jsbin.com/alabob/70/edit

Related articles within this topic

Sebastian Seilund

About the author

Sebastian Seilund is one of the founders of Billy’s Billing and is the CTO of the company. Sebastian has a broad experience of developing user friendly web applications like Billy’s Billing. Follow Sebastian on Twitter, or read more at sebastianseilund.com.

32 comments

Guilherme Aiolfi
1 year ago

Hi, I was trying to change your code to check parent nodes when checking a node and notice that if you check a node and hide it (isExpanded to false in its parent), when showing again, it comes back unchecked.

Otherwide, great approach. :)

Guilherme Aiolfi
1 year ago

So, after some digging: http://jsbin.com/alabob/11

But I'm not happy with the result because:
1) I had to include a view state (checked) inside the model class
2) it's about time for @each observers pass changed views, right? ember core developers should take care of that before 1.0
3) it would much easier to get the reference of a parent controller, but I couldn't do it :T

I'll keep trying and post here if I got lucky.

Sebastian Seilund
1 year ago

@ Guilherme Good catch! I've updated the examples, so collapsing checked items works. It was straightforward to do with a contentDidChange observer.

Guilherme Aiolfi
1 year ago

You can replace:

if (!selectedNodes.contains(node)) selectedNodes.pushObject(node);

With simply:

selectedNodes.addObject(node);

.addObjects keeps just one instance of each object.

Sebastian Seilund
1 year ago

@Guilherme

1. Yeah, the `checked` property doesn't really belong in the model. It should stay on the controller.
2. Can you elaborate on this one?
3. You would need a mechanism for getting a controller by node. Something like this: http://jsbin.com/alabob/15/edit (it's not a full solution)

Guilherme Aiolfi
1 year ago

Sebastian, sorry, I think I wasn't clear. Going down the tree is not a problem since you have the reference for children. The problem is checking all parents for a given node.

For example, when checking "Tops", check "Women" and "Clothes".

Sebastian Seilund
1 year ago

You need to have each node to know of its parent. Something like this: http://jsbin.com/alabob/17/edit

See the `setParentsForTree` function. Right now the `bubbleChecked` is too aggressive. But you get the point :)

Guilherme Aiolfi
1 year ago

Thanks, that will work for now. But that's not that easy anymore. You had to change your model (include the parent reference) and have done quiet a magic registering/unregistering.

I hope Tom Dale or Yahuda Katz are paying attention for those cases and it will be easier along the way.

I'm investing heavily in ember.js, so, I wish it the best.

And thank you again, Sebastian, for taking the time for doing all you've done.

Adam Robertson
1 year ago

NIcely done! I've had fun creating custom UI elements with Ember.js, glad to see you were able to do it so elegantly with a tree view as well!

james
12 months ago

Great article, thanks. Not directly related, but since you are conversant with emberjs, do you think it is possible to share an emberjs app so it can be embedded in other sites the way you just embedded jsbin here. I want to build a calendar app and I am considering emberjs but I want people to embed that calendar in their site as 3rd party javascript just like how adsense is embedded in other sites.

Will emberjs be suitable for this sort of thing and do you have any guidance or links that might be helpful.

Sebastian Seilund
11 months ago

@james: Thanks for your comment. If you embed it using an iframe (like jsbin is here) you can use Ember.js all you want. One thing to be aware of is the total filesize. Your users will want your plugin to be as lightweight as possible. The Ember.js library isn't that big, so if you have only a few jpg or pngs the JS size will be of less importance.

james
11 months ago

Thanks Sebastian for the response.

Enric Ribas
11 months ago

Awesome! I was trying to do something similar and didn't know about {{control}}. This is much easier. :)
BTW: I think the JSbins are not working at the moment, but they were a few days ago.

Dan Mazzini
9 months ago

The problem in jsfiddle is that this doesn't work with the jquery latest. To fix it, specify a jquery version, eg: http://ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js

sbr
9 months ago

This is great. Can you help me understand what it would take to get the data from a rest service?

ken
9 months ago

Very nice write up, thanks for contributing it. For some reason the JSBin included doesn't work though. Any thoughts on getting that back to a happy place?

Jean
9 months ago

This is an excellent approach. Well done.

Sebastian Seilund
9 months ago

Thanks for the heads-up. I've updated the samples to work with Ember.js RC 6.1 and jQuery 2.0.

If anybody are building a tree today I would checkout the new `Ember.Component` (http://emberjs.com/api/classes/Ember.Component.html) feature. It's much more suitable for applications like this.

Sebastian Seilund
9 months ago

@sbr: If you have a REST endpoint that responds with tree data all you would have to do is:

$.getJSON('/api/tree_example', function(payload) {
App.set('treeRoot', payload);
});

Jean de Klerk
8 months ago

Hi all - not sure if there is still work on this, but I've added a github repo with this code (attributed to seilund). I've also fixed the bubbleChecked to not aggressively check everything - it instead checks parents whose children are all checked.

The repo is here: https://github.com/jadekler/git-ember-treetable

I will be working 'expand all / collapse all' functionality - a naive approach of simply checking / unchecking everything is incredibly slow, so I will probably modify this code to use css display: none instead of dom manipulation by ember. If anyone has worked on this problem, please share ideas + thoughts or even perform a pull request on the repo.

Jean de Klerk
8 months ago

Quick update: Have continued on Sebastian's code to include correct bubble/cascade checking, 'Expand/Collapse all' functionality, and revealing a node within the tree (e.g. all parents above some node are expanded).

Also, the use of DOM to remove / add nodes has been changed to a CSS display:none approach for closed nodes to improve on speed (although the long load on large trees is still present during initial load - any help on that appreciated).

https://github.com/jadekler/git-ember-treetable

Thanks again for the good work Sebastian.

Sebastian Seilund
8 months ago

The CSS display vs. DOM add/remove is a tricky issue. It depends on the size and structure of your tree. Imagine that you had a binary tree of 2^10 nodes. That's a thousand views you need to insert which will be slow. Displaying the root two would be better. Maybe the best solution would be to put all visible nodes + 1 level further in DOM and use CSS display to hide the last collapsed level.

Jean de Klerk
8 months ago

Re: inserting 2^10 nodes - that is the problem I ran into with the expand all button. My solution puts all the loading at the front load and then uses display:none for additional expanding/collapse operations.

Without redesigning the control recursive structure, which creates a view for every node in the data, I'm not sure how I could avoid that basic problem of displaying everything at once.

Re: displaying the root two + putting visible nodes + 1 level further in dom - this is an interesting idea but I'm not sure it would alleviate the problem of inserting that many views.

MBehtemam
7 months ago

i try to implement your way but when click on one of root element all element is opend:
http://jsbin.com/IbOVEGo/5/
how can i fix this problem .
thanks

Alex
7 months ago

Hi Sebastian,

thank you for this great article. I got this working but I need one thing I can't seem to figure out... how is it possible to have only one expanded node? Let's say I have expanded Fruits. Now I leave Fruits open and expand People. I want Fruits to automatically close now.

In theory, before one node gets expanded I have to loop over all nodes and set isExpanded to false but where and how?

Alex
7 months ago

I figured it out. In the controller's action/toggle function:

// set all other nodes isExpanded = false
this.get('target').forEach(function(controller) {
controller.set('isExpanded', false);
});
// set current node isExpanded = true
this.set('isExpanded', !this.get('isExpanded'));

Aman
5 months ago

I would like to add an onclick event on the tree nodes , But as all the document is not loaded , Only the Top level nodes in the hierarchy are set with the event .

I even tried writing my onclick code in the click method , But i need the UI anchor tag there to perform my operation , whereas the method gets the object .

Could someone please help me understand how i can resolve this issue.

Devin
2 months ago

I tried updating ember and handlebars to the latest version using your example and now i'm getting this error:

subclass of Ember.View):ember208> Handlebars error: Could not find property 'control' on object (generated application controller).

Any idea how to fix? I am adding this before I include ember:

window.ENV = window.ENV || {};
ENV.EXPERIMENTAL_CONTROL_HELPER = true;

Sebastian Seilund
2 months ago

Hi Devin. The `{{control}}` helper has been removed from Ember AFAIK. Check out the newer example using `Ember.Component`: http://jsbin.com/alabob/70/edit

Devin
2 months ago

That worked great - even less code now! Thanks Sebastian!

Devin
2 months ago

How would we remove a node from the tree here?

Something like this.removeObject(this.get("node")) but it doesn't work - maybe I have to obtain a reference to the parent node and then remove it's child?

Christian Genco
4 days ago

Is there a way to do this without components? I think `render` should be able to, but when I try I get "Maximum call stack size exceeded errors"

Write a comment

Your name*
Your email*
Comment*
Notify me via email when there are new comments on this post