Using Recursive Components in Ember.js

Omar Ismail

We recently built an analytics engine for MIT Media Lab using Ember.js, Rails, and D3.js. The project involved querying the MIT’s servers for data and using D3 to visualize the streams of data that returned from the query. Each of those streams could be operated on (e.g. finding the average), which would render a new stream showing the altered data.

How We Built The Tree Structure

We built a tree structure where the root node was the initial stream from the query, and child nodes were new streams which were generated as a result of a specific operation applied to a parent node. Sibling nodes — nodes that have the same parent — could be created as a result of applying multiple operations on the same parent node.

To accomplish this, we created a node model, which has many of itself (children), and belongs to itself (parent). In modeling terms, this is called a reflexive association.

export default DS.Model.extend({
  // other domain logic

  children: DS.hasMany("nodes", { inverse: "parent" }),
  parent: DS.belongsTo("node", { inverse: "children" })
});

Recursive Calling of a Component

Tree

Rendering a tree structure of unknown size can be done by using recursion. The best way to handle this in Ember is to create a component — representing a node on a tree — that recursively calls itself. In the template that retrieved the initial data stream, we called the component that began the root of the tree.

{{stream-node node=model.rootNode}}

At the bottom of the {{stream-node}} component’s template, we iterated through the children on the model — the node — and rendered a new {{stream-node}} component for each.

{{#each displayedChildren as |displayedChild|}}
  {{stream-node node=displayedChild stackedSiblings=stackedChildren}}
{{/each}}

And in our {{stream-node}} component:

export default Ember.Component.extend({
  // other domain logic

  displayedChildren: function() {
    return this.get("node.children").filter(function(node) {
      return node.get("isTopChild");
    });
  }.property("node.{children,topChild}"),
});

A few things are happening here. Every time we call {{stream-node}}, we pass in the new stream data as node=…. We can now use the same functionality on all the nodes while only defining that functionality once in the {{stream-node}} component.

The tree was represented vertically down the page. Each child node was placed below the parent node. Whenever a node had siblings, they were displayed as a stack, and the topChild() was the node that appeared at the top of the stack. We had a separate page for comparing the sibling nodes, and the compare view could alter what the top child was.

Compare

topChild is an attribute on the Node model rather than the component because it is part of the domain logic, in which the server uses to figure out what the active leaf nodes are for a given tree. The topChild value is also used in many places within the app, and therefore cannot be encapsulated in a single component.

Because of the association on the parent node to the child node, the parent node can validate that only one of its children has the topChild attribute set to true. This also gave us the benefit that when the model changes (adding children or altering the topChild), the computed properties will handle the update and display the proper information.

Because we have the property("node.children") on displayedChildren() function, any time a new child was added to a node, Ember automatically created a new {{stream-node}} component and appended the appropriate node at the bottom of the page.

When creating a new child node, all we needed to do to ensure that Ember would create the new component automatically was push the new node on the children array of the parent node:

newNode.save().then((node) => {
  parentNode.get("children").addObject(node);
});

Bubbling and Deleting Nodes

Bubbling up events/actions in components is different than bubbling up via controller/routes. Controller/route bubbling happens automatically. Components are isolated, which means that when bubbling up actions is done manually. The name of the action in the controller/route that the event ultimately bubbles up to needs to be assigned as an attribute on the component so that Ember knows which function in the actions hash should handle this event.

The delete action is triggered within the stream-node template.

<a href="#" class="delete button button-tile button-default" {{action
"deleteNode" node}}>
</a>

There are two ways to assign this attribute. One is on the component call in the template {{stream-node deleteNode="deleteNode" node=model.rootNode}}, and the other is to assign the attribute within the component itself:

export default Ember.Component.extend({
  deleteNode: "deleteNode",

  // other domain logic

  actions: {
    deleteNode: function(node) {
      this.sendAction("deleteNode", node);
    }
  }
});

This would keep bubbling up through the component tree until it hit the action in the route:

export default Ember.Route.extend({
  // other domain logic

  actions: {
    deleteNode: function(node) {
      node.destroyRecord();
    }
  }
});

At this point, if you don’t delete the child nodes, then the orphaned nodes will remain in the store, but won’t be displayed. Once a child is deleted, the model of the parent node will update due to computed properties, and the components will re-render.

All in all, this project displayed the power of Ember in maintaining consistency in the data and updating computed properties whenever data changed. When we were tasked to implement multiple trees — a task we thought would take us a long time — it took 40 minutes to build, thanks to the heavy lifting of Ember.