Mostly about Javascript, Ruby on Rails and other web stuff

Sebastian's Blog

Building Nested Recursive Directives in Angular

I learnt a new trick over the weekend using Angular, how to build a recursive tree of objects using directives. In this post I want to share how to do it.

Let’s say that you have some data that looks like this, it can be as deep as you want:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   [
      {    name: 'Europe',
          children: [
              {    name: 'Italy',
                  children: [
                      {    name: 'Rome' },
                      {    name: 'Milan'    }
                  ]},
              {    name: 'Spain'}
          ]
      },
      {    name: 'South America',
          children: [
              {    name: 'Brasil'   },
              {    name: 'Peru' }
          ]
      }
  ];

And using this data you want to build a tree, e.g.:

Europe
    Italy
        Rome
        Milan
    Spain
South America
    Brasil
    Peru

So to build something like this you will need some kind of recursive code to loop over all the elements and their children.

Let’s start with the html:

1
2
3
4
5
6
7
<html ng-app='APP'>
  ...
  <div ng-controller="IndexCtrl">
  
  </div>
  ...
</html>

First we have a controller ‘IndexCtrl’ which looks like this:

1
2
3
4
5
6
var app = angular.module('APP', []);


app.controller('IndexCtrl', function ($scope) {
  $scope.locations = [ ..this is the array of locations shown above ..];
});

Here we have $scope.locations pointing to the array of locations we want to render in our tree.

Then we need a directive for rendering a collection, the html for the collection looks like this:

1
2
3
<div ng-controller="IndexCtrl">
  <collection collection='locations'></collection>
</div>

This directive takes a collection parameter which are the locations we want to render.

Our directive definition looks like this:

1
2
3
4
5
6
7
8
9
10
app.directive('collection', function () {
  return {
      restrict: "E",
      replace: true,
      scope: {
          collection: '='
      },
      template: "<ul><member ng-repeat='member in collection' member='member'></member></ul>"
  }
})
  • restrict: ‘E’ tells angular that we want to apply this directive to any html tags matching collection.
  • replace: true tells angular that we want to replace the tag with the content of specified template in the directive.
  • scope: creates a new scope for the directive.
  • collection: ‘=’ tells angular to use the collection attribute in the directive and create a variable in the directive scope with the same name, = means that it should be passed as an object.
  • template: “…” is the new html that will be inserted instead of the original tag.

Notice the following code in the template:

1
<member ng-repeat='member in collection' member='member'></member>

This is another directive, used to render each member of the collection (we use the build-in ng-repeat for looping), in this directive we pass the current member as the member attribute.

The directive for member looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.directive('member', function ($compile) {
  return {
      restrict: "E",
      replace: true,
      scope: {
          member: '='
      },
      template: "<li></li>",
      link: function (scope, element, attrs) {
          if (angular.isArray(scope.member.children)) {
              element.append("<collection collection='member.children'></collection>");
              $compile(element.contents())(scope)
          }
      }
  }
})

This looks a lot like the collection directive, except for the link function. I will explain what is happening here, but before that, let me tell you about my first approach for trying to do this.

The first thing I tried is to simply add the collection directive inside the template:

1
template: "<li> <collection collection='member.children'></collection></li>"

But this doesn’t work, it throws angular into an endless loop, because it tries to render the collection directive regardless if the member has children or not.

So instead you need to add the collection directive manually only if there are children, thus the link function:

1
2
3
4
5
6
7
8
9
link: function (scope, element, attrs) {
  //check if this member has children
  if (angular.isArray(scope.member.children)) {
      // append the collection directive to this element
      element.append("<collection collection='member.children'></collection>");
      // we need to tell angular to render the directive
      $compile(element.contents())(scope);
  }
}

Note the line $compile(element.contents())(scope);. As the html is appended manually we need to tell angular to re-render the directive.

That is it, here is the complete example. Thanks.

Update 2013-12-31: Please read the comments, there are some good suggestions on how to do this better.

Comments