Recursive directives
The power of directives can also be effectively applied when consuming data in a more unwieldy format. Consider the case in which you have a JavaScript object that exists in some sort of recursive tree structure. The view that you will generate for this object will also reflect its recursive nature and will have nested HTML elements that match the underlying data structure.
Getting ready
Suppose you had a recursive data object in your controller as follows:
(app.js) angular.module('myApp', []) .controller('MainCtrl', function($scope) { $scope.data = { text: 'Primates', items: [ { text: 'Anthropoidea', items: [ { text: 'New World Anthropoids' }, { text: 'Old World Anthropoids', items: [ { text: 'Apes', items: [ { text: 'Lesser Apes' }, { text: 'Greater Apes' } ] }, { text: 'Monkeys' } ] } ] }, { text: 'Prosimii' } ] }; });
How to do it…
As you might imagine, iteratively constructing a view or only partially using directives to accomplish this will become extremely messy very quickly. Instead, it would be better if you were able to create a directive that would seamlessly break apart the data recursively, and define and render the sub-HTML fragments cleanly. By cleverly using directives and the $compile
service, this exact directive functionality is possible.
The ideal directive in this scenario will be able to handle the recursive object without any additional parameters or outside assistance in parsing and rendering the object. So, in the main view, your directive will look something like this:
<recursive value="nestedObject"></recursive>
The directive is accepting an isolate scope =
binding to the parent scope object, which will remain structurally identical as the directive descends through the recursive object.
The $compile service
You will need to inject the $compile
service in order to make the recursive directive work. The reason for this is that each level of the directive can instantiate directives inside it and convert them from an uncompiled template to real DOM material.
The angular.element() method
The angular.element()
method can be thought of as the jQuery $()
equivalent. It accepts a string template or DOM fragment and returns a jqLite object that can be modified, inserted, or compiled for your purposes. If the jQuery library is present when the application is initialized, AngularJS will use that instead of jqLite. If you use the AngularJS template cache, retrieved templates will already exist as if you had called the angular.element()
method on the template text.
The $templateCache
Inside a directive, it's possible to create a template using angular.element()
and a string of HTML similar to an underscore.js
template. However, it's completely unnecessary and quite unwieldy to use compared to AngularJS templates. When you declare a template and register it with AngularJS, it can be accessed through the injected $templateCache
, which acts as a key-value store for your templates.
The recursive template is as follows:
<script type="text/ng-template" id="recursive.html"> <span>{{ val.text }}</span> <button ng-click="delSubtree()">delete</button> <ul ng-if="isParent" style="margin-left:30px"> <li ng-repeat="item in val.items"> <tree val="item" parent-data="val.items"></tree> </li> </ul> </script>
The <span>
and <button>
elements are present at each instance of a node, and they present the data at that node as well as an interface to the click event (which we will define in a moment) that will destroy it and all its children.
Following these, the conditional <ul>
element renders only if the isParent
flag is set in the scope, and it repeats through the items array, recursing the child data and creating new instances of the directive. Here, you can see the full template definition of the directive:
<tree val="item" parent-data="val.items"></tree>
Not only does the directive take a val
attribute for the local node data, but you can also see its parent-data
attribute, which is the point of scope indirection that allows the tree structure. To make more sense of this, examine the following directive code:
(app.js) .directive('tree', function($compile, $templateCache) { return { restrict: 'E', scope: { val: '=', parentData: '=' }, link: function(scope, el, attrs) { scope.isParent = angular.isArray(scope.val.items) scope.delSubtree = function() { if(scope.parentData) { scope.parentData.splice( scope.parentData.indexOf(scope.val), 1 ); } scope.val={}; } el.replaceWith( $compile( $templateCache.get('recursive.html') )(scope) ); } }; });
With all of this, if you provide the recursive directive with the data object provided at the beginning of this recipe, it will result in the following (presented here without the auto-added AngularJS comments and directives):
(index.html – uncompiled) <div ng-app="myApp"> <div ng-controller="MainCtrl"> <tree val="data"></tree> </div> <script type="text/ng-template" id="recursive.html"> <span>{{ val.text }}</span> <button ng-click="deleteSubtree()">delete</button> <ul ng-if="isParent" style="margin-left:30px"> <li ng-repeat="item in val.items"> <tree val="item" parent-data="val.items"></tree> </li> </ul> </script> </div>
The recursive nature of the directive templates enables nesting, and when compiled using the recursive data object located in the wrapping controller, it will compile into the following HTML:
(index.html - compiled) <div ng-controller="MainController"> <span>Primates</span> <button ng-click="delSubtree()">delete</button> <ul ng-if="isParent" style="margin-left:30px"> <li ng-repeat="item in val.items"> <span>Anthropoidea</span> <button ng-click="delSubtree()">delete</button> <ul ng-if="isParent" style="margin-left:30px"> <li ng-repeat="item in val.items"> <span>New World Anthropoids</span> <button ng-click="delSubtree()">delete</button> </li> <li ng-repeat="item in val.items"> <span>Old World Anthropoids</span> <button ng-click="delSubtree()">delete</button> <ul ng-if="isParent" style="margin-left:30px"> <li ng-repeat="item in val.items"> <span>Apes</span> <button ng-click="delSubtree()">delete</button> <ul ng-if="isParent" style="margin-left:30px"> <li ng-repeat="item in val.items"> <span>Lesser Apes</span> <button ng-click="delSubtree()">delete</button> </li> <li ng-repeat="item in val.items"> <span>Greater Apes</span> <button ng-click="delSubtree()">delete</button> </li> </ul> </li> <li ng-repeat="item in val.items"> <span>Monkeys</span> <button ng-click="delSubtree()">delete</button> </li> </ul> </li> </ul> </li> <li ng-repeat="item in val.items"> <span>Prosimii</span> <button ng-click="delSubtree()">delete</button> </li> </ul> </div>
Tip
JSFiddle: http://jsfiddle.net/msfrisbie/ka46yx4u/
How it works…
The definition of the isolate scope through the nested directives described in the previous section allows all or part of the recursive objects to be bound through parentData
to the appropriate directive instance, all the while maintaining the nested connectedness afforded by the directive hierarchy. When a parent node is deleted, the lower directives are still bound to the data object and the removal propagates through cleanly.
The meatiest and most important part of this directive is, of course, the link
function. Here, the link
function determines whether the node has any children (which simply checks for the existence of an array in the local data node) and declares the deleting method, which simply removes the relevant portion from the recursive object and cleans up the local node. Up until this point, there haven't been any recursive calls, and there shouldn't need to be. If your directive is constructed correctly, AngularJS data binding and inherent template management will take care of the template cleanup for you. This, of course, leads into the final line of the link
function, which is broken up here for readability:
el.replaceWith( $compile( $templateCache.get('recursive.html') )(scope) );
Recall that in a link
function, the second parameter is the jqLite-wrapped DOM object that the directive is linking—here, the <tree>
element. This exposes to you a subset of jQuery object methods, including replaceWith()
, which you will use here. The top-level instance of the directive will be replaced by the recursively-defined template, and this will carry down through the tree.
At this point, you should have an idea of how the recursive structure is coming together. The element parameter needs to be replaced with a recursively-compiled template, and for this, you will employ the $compile
service. This service accepts a template as a parameter and returns a function that you will invoke with the current scope inside the directive's link
function. The template is retrieved from $templateCache
by the recursive.html
key, and then it's compiled. When the compiler reaches the nested <tree>
directive, the recursive directive is realized all the way down through the data in the recursive object.
There's more…
This recipe demonstrates the power of constructing a directive to convert a complex data object into a large DOM object. Relevant portions can be broken into individual templates, handled with distributed directive logic, and combined together in an elegant fashion to maximize modularity and reusability.
See also
- The Optional nested directive controllers recipe covers vertical communication between directives through their controller objects