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:
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.
As of Angular 1.5.x, no more tricks are required, the following has been made possible. No more need for dirty work arounds!
This discovery was a by product of my hunt for a better/cleaner solution for a recursive directive. You can find it here https://jsfiddle.net/cattails27/5j5au76c/. It supports as far is 1.3.x.
angular.element(document).ready(function() {
angular.module('mainApp', [])
.controller('mainCtrl', mainCtrl)
.directive('recurv', recurveDirective);
angular.bootstrap(document, ['mainApp']);
function recurveDirective() {
return {
template: '<ul><li ng-repeat="t in tree">{{t.sub}}<recurv tree="t.children"></recurv></li></ul>',
scope: {
tree: '='
},
}
}
});
function mainCtrl() {
this.tree = [{
title: '1',
sub: 'coffee',
children: [{
title: '2.1',
sub: 'mocha'
}, {
title: '2.2',
sub: 'latte',
children: [{
title: '2.2.1',
sub: 'iced latte'
}]
}, {
title: '2.3',
sub: 'expresso'
}, ]
}, {
title: '2',
sub: 'milk'
}, {
title: '3',
sub: 'tea',
children: [{
title: '3.1',
sub: 'green tea',
children: [{
title: '3.1.1',
sub: 'green coffee',
children: [{
title: '3.1.1.1',
sub: 'green milk',
children: [{
title: '3.1.1.1.1',
sub: 'black tea'
}]
}]
}]
}]
}];
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<div>
<div ng-controller="mainCtrl as vm">
<recurv tree="vm.tree"></recurv>
</div>
</div>
After using several workarounds for a while, I've repeatedly come back to this issue.
I'm not satisfied by the service solution since it works for directives that can inject the service but does not work for anonymous template fragments.
Similarly, solutions which depend upon specific template structure by doing DOM manipulation in the directive are too specific and brittle.
I have what I believe is a generic solution that encapsulates the recursion as a directive of its own that interferes minimally with any other directives and can be used anonymously.
Below is a demonstration that you can also play around with at plnkr: http://plnkr.co/edit/MSiwnDFD81HAOXWvQWIM
var hCollapseDirective = function () {
return {
link: function (scope, elem, attrs, ctrl) {
scope.collapsed = false;
scope.$watch('collapse', function (collapsed) {
elem.toggleClass('collapse', !!collapsed);
});
},
scope: {},
templateUrl: 'collapse.html',
transclude: true
}
}
var hRecursiveDirective = function ($compile) {
return {
link: function (scope, elem, attrs, ctrl) {
ctrl.transclude(scope, function (content) {
elem.after(content);
});
},
controller: function ($element, $transclude) {
var parent = $element.parent().controller('hRecursive');
this.transclude = angular.isObject(parent)
? parent.transclude
: $transclude;
},
priority: 500, // ngInclude < hRecursive < ngIf < ngRepeat < ngSwitch
require: 'hRecursive',
terminal: true,
transclude: 'element',
$$tlb: true // Hack: allow multiple transclusion (ngRepeat and ngIf)
}
}
angular.module('h', [])
.directive('hCollapse', hCollapseDirective)
.directive('hRecursive', hRecursiveDirective)
/* Demo CSS */
* { box-sizing: border-box }
html { line-height: 1.4em }
.task h4, .task h5 { margin: 0 }
.task { background-color: white }
.task.collapse {
max-height: 1.4em;
overflow: hidden;
}
.task.collapse h4::after {
content: '...';
}
.task-list {
padding: 0;
list-style: none;
}
/* Collapse directive */
.h-collapse-expander {
background: inherit;
position: absolute;
left: .5px;
padding: 0 .2em;
}
.h-collapse-expander::before {
content: '•';
}
.h-collapse-item {
border-left: 1px dotted black;
padding-left: .5em;
}
.h-collapse-wrapper {
background: inherit;
padding-left: .5em;
position: relative;
}
<!DOCTYPE html>
<html>
<head>
<link href="collapse.css" rel="stylesheet" />
<link href="style.css" rel="stylesheet" />
<script data-require="[email protected]" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js" data-semver="2.1.1" data-require="jquery@*"></script>
<script src="script.js"></script>
<script>
function AppController($scope) {
$scope.toggleCollapsed = function ($event) {
$event.preventDefault();
$event.stopPropagation();
this.collapsed = !this.collapsed;
}
$scope.task = {
name: 'All tasks',
assignees: ['Citizens'],
children: [
{
name: 'Gardening',
assignees: ['Gardeners', 'Horticulture Students'],
children: [
{
name: 'Pull weeds',
assignees: ['Weeding Sub-committee']
}
],
},
{
name: 'Cleaning',
assignees: ['Cleaners', 'Guests']
}
]
}
}
angular.module('app', ['h'])
.controller('AppController', AppController)
</script>
</head>
<body ng-app="app" ng-controller="AppController">
<h1>Task Application</h1>
<p>This is an AngularJS application that demonstrates a generalized
recursive templating directive. Use it to quickly produce recursive
structures in templates.</p>
<p>The recursive directive was developed in order to avoid the need for
recursive structures to be given their own templates and be explicitly
self-referential, as would be required with ngInclude. Owing to its high
priority, it should also be possible to use it for recursive directives
(directives that have templates which include the directive) that would
otherwise send the compiler into infinite recursion.</p>
<p>The directive can be used alongside ng-if
and ng-repeat to create recursive structures without the need for
additional container elements.</p>
<p>Since the directive does not request a scope (either isolated or not)
it should not impair reasoning about scope visibility, which continues to
behave as the template suggests.</p>
<p>Try playing around with the demonstration, below, where the input at
the top provides a way to modify a scope attribute. Observe how the value
is visible at all levels.</p>
<p>The collapse directive is included to further demonstrate that the
recursion can co-exist with other transclusions (not just ngIf, et al)
and that sibling directives are included on the recursive due to the
recursion using whole 'element' transclusion.</p>
<label for="volunteer">Citizen name:</label>
<input id="volunteer" ng-model="you" placeholder="your name">
<h2>Tasks</h2>
<ul class="task-list">
<li class="task" h-collapse h-recursive>
<h4>{{task.name}}</h4>
<h5>Volunteers</h5>
<ul>
<li ng-repeat="who in task.assignees">{{who}}</li>
<li>{{you}} (you)</li>
</ul>
<ul class="task-list">
<li h-recursive ng-repeat="task in task.children"></li>
</ul>
<li>
</ul>
<script type="text/ng-template" id="collapse.html">
<div class="h-collapse-wrapper">
<a class="h-collapse-expander" href="#" ng-click="collapse = !collapse"></a>
<div class="h-collapse-item" ng-transclude></div>
</div>
</script>
</body>
</html>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With