Recursion in Angular directives
Inspired by the solutions described in the thread mentioned by @dnc253, I abstracted the recursion functionality into a service.
module.factory('RecursionHelper', ['$compile', function($compile){ return { /** * Manually compiles the element, fixing the recursion loop. * @param element * @param [link] A post-link function, or an object with function(s) registered via pre and post properties. * @returns An object containing the linking functions. */ compile: function(element, link){ // Normalize the link parameter if(angular.isFunction(link)){ link = { post: link }; } // Break the recursion loop by removing the contents var contents = element.contents().remove(); var compiledContents; return { pre: (link && link.pre) ? link.pre : null, /** * Compiles and re-adds the contents */ post: function(scope, element){ // Compile the contents if(!compiledContents){ compiledContents = $compile(contents); } // Re-add the compiled contents to the element compiledContents(scope, function(clone){ element.append(clone); }); // Call the post-linking function, if any if(link && link.post){ link.post.apply(null, arguments); } } }; } };}]);
Which is used as follows:
module.directive("tree", ["RecursionHelper", function(RecursionHelper) { return { restrict: "E", scope: {family: '='}, template: '<p>{{ family.name }}</p>'+ '<ul>' + '<li ng-repeat="child in family.children">' + '<tree family="child"></tree>' + '</li>' + '</ul>', compile: function(element) { // Use the compile function from the RecursionHelper, // And return the linking function(s) which it returns return RecursionHelper.compile(element); } };}]);
See this Plunker for a demo.I like this solution best because:
- You don't need an special directive which makes your html less clean.
- The recursion logic is abstracted away into the RecursionHelper service, so you keep your directives clean.
Update: As of Angular 1.5.x, no more tricks are required, but works only with template, not with templateUrl
Manually adding elements and compiling them is definitely a perfect approach. If you use ng-repeat then you will not have to manually remove elements.
Demo: http://jsfiddle.net/KNM4q/113/
.directive('tree', function ($compile) {return { restrict: 'E', terminal: true, scope: { val: '=', parentData:'=' }, link: function (scope, element, attrs) { var template = '<span>{{val.text}}</span>'; template += '<button ng-click="deleteMe()" ng-show="val.text">delete</button>'; if (angular.isArray(scope.val.items)) { template += '<ul class="indent"><li ng-repeat="item in val.items"><tree val="item" parent-data="val.items"></tree></li></ul>'; } scope.deleteMe = function(index) { if(scope.parentData) { var itemIndex = scope.parentData.indexOf(scope.val); scope.parentData.splice(itemIndex,1); } scope.val = {}; }; var newElement = angular.element(template); $compile(newElement)(scope); element.replaceWith(newElement); }}});
I don't know for sure if this solution is found in one of the examples you linked or the same basic concept, but I had a need of a recursive directive, and I found a great, easy solution.
module.directive("recursive", function($compile) { return { restrict: "EACM", priority: 100000, compile: function(tElement, tAttr) { var contents = tElement.contents().remove(); var compiledContents; return function(scope, iElement, iAttr) { if(!compiledContents) { compiledContents = $compile(contents); } iElement.append( compiledContents(scope, function(clone) { return clone; })); }; } };});module.directive("tree", function() { return { scope: {tree: '='}, template: '<p>{{ tree.text }}</p><ul><li ng-repeat="child in tree.children"><recursive><span tree="child"></span></recursive></li></ul>', compile: function() { return function() { } } };});
You should create the recursive
directive and then wrap it around the element that makes the recursive call.