Javascript Angular 指令中的递归
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/14430655/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Recursion in Angular directives
提问by Benny Bottema
There are a couple of popular recursive angular directive Q&A's out there, which all come down to one of the following solutions:
有几个流行的递归角度指令问答,它们都归结为以下解决方案之一:
- manually incrementally 'compile' HTML based on runtime scope state
- don't use a directive at all, but a <script> template which refers to itself
- 根据运行时范围状态手动增量“编译”HTML
- 根本不使用指令,而是使用引用自身的 <script> 模板
The first onehas the problem that you can't remove previously compiled code unless you comprehensibly manage the manual compile process. The second approachhas the problem of... not being a directive and missing out on its powerful capabilities, but more urgently, it can't be parameterised the same way a directive can be; it's simply bound to a new controller instance.
第一个存在的问题是,除非您对手动编译过程进行了全面的管理,否则您无法删除以前编译的代码。第二种方法的问题是......不是指令并错过了其强大的功能,但更紧急的是,它不能像指令一样参数化;它只是绑定到一个新的控制器实例。
I've been playing with manually doing an angular.bootstrapor @compile()in the link function, but that leaves me with the problem of manually keeping track of elements to remove and add.
我一直在玩手动执行angular.bootstrap或@compile()在链接功能中,但这给我留下了手动跟踪要删除和添加的元素的问题。
Is there a good way to have a parameterized recursive pattern that manages adding/removing elements to reflect runtime state? That is to say, a tree with a add/delete node button and some input field whose value is passed down a node's child nodes. Perhaps a combination of the second approach with chained scopes (but I have no idea how to do this)?
有没有一种好的方法来管理添加/删除元素以反映运行时状态的参数化递归模式?也就是说,带有添加/删除节点按钮和一些输入字段的树,其值向下传递节点的子节点。也许第二种方法与链式作用域的结合(但我不知道如何做到这一点)?
回答by Mark Lagendijk
Inspired by the solutions described in the thread mentioned by @dnc253, I abstracted the recursion functionality into a service.
受到@dnc253 提到的线程中描述的解决方案的启发,我将递归功能抽象为一个 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 Plunkerfor a demo. I like this solution best because:
有关演示,请参阅此Plunker。我最喜欢这个解决方案,因为:
- 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.
- 你不需要一个特殊的指令来让你的 html 不那么干净。
- 递归逻辑被抽象到 RecursionHelper 服务中,因此您可以保持指令干净。
Update: As of Angular 1.5.x, no more tricks are required, but works only with template, not with templateUrl
更新:从 Angular 1.5.x 开始,不再需要任何技巧,但仅适用于template,不适用于templateUrl
回答by SunnyShah
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.
手动添加元素并编译它们绝对是一种完美的方法。如果您使用 ng-repeat 那么您将不必手动删除元素。
Demo: http://jsfiddle.net/KNM4q/113/
演示: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);
}
}
});
回答by dnc253
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 recursivedirective and then wrap it around the element that makes the recursive call.
您应该创建recursive指令,然后将它包裹在进行递归调用的元素周围。
回答by jkris
As of Angular 1.5.x, no more tricks are required, the following has been made possible. No more need for dirty work arounds!
从 Angular 1.5.x 开始,不再需要任何技巧,以下已成为可能。不再需要肮脏的工作!
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.
这个发现是我为递归指令寻找更好/更干净的解决方案的副产品。你可以在这里找到它https://jsfiddle.net/cattails27/5j5au76c/。它支持 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>
回答by tilgovi
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.
同样,通过在指令中进行 DOM 操作而依赖于特定模板结构的解决方案过于具体和脆弱。
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
下面是一个演示,你也可以在 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>
回答by TGH
Now that Angular 2.0 is out in preview I think it's ok to add an Angular 2.0 alternative into the mix. At least it will benefit people later:
现在 Angular 2.0 已发布预览版,我认为可以将 Angular 2.0 替代品添加到组合中。至少以后会造福于人:
The key concept is to build a recursive template with a self reference:
关键概念是构建一个带有自引用的递归模板:
<ul>
<li *for="#dir of directories">
<span><input type="checkbox" [checked]="dir.checked" (click)="dir.check()" /></span>
<span (click)="dir.toggle()">{{ dir.name }}</span>
<div *if="dir.expanded">
<ul *for="#file of dir.files">
{{file}}
</ul>
<tree-view [directories]="dir.directories"></tree-view>
</div>
</li>
</ul>
You then bind a tree object to the template and watch the recursion take care of the rest. Here is a full example: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0
然后将树对象绑定到模板并观察递归处理其余部分。这是一个完整的例子:http: //www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0
回答by erobwen
There is a really really simple workaround for this that does not require directives at all.
有一个非常简单的解决方法,根本不需要指令。
Well, in that sense, maybe it is not even a solution of the original problem if you assume you need directives, but it IS a solution if you want a recursive GUI structure with parametrized sub-structures of the GUI. Which is probably what you want.
好吧,从这个意义上说,如果您假设您需要指令,它甚至不是原始问题的解决方案,但是如果您想要具有 GUI 参数化子结构的递归 GUI 结构,它是一个解决方案。这可能是你想要的。
The solution is based on just using ng-controller, ng-init and ng-include. Just do it as follows, assume that your controller is called "MyController", your template is located in myTemplate.html and that you have an initialization function on your controller called init that takes argument A, B, and C, making it possible to parametrize your controller. Then the solution is as follows:
该解决方案基于仅使用 ng-controller、ng-init 和 ng-include。只需按如下方式进行,假设您的控制器名为“MyController”,您的模板位于 myTemplate.html 中,并且您的控制器上有一个名为 init 的初始化函数,它接受参数 A、B 和 C,从而可以参数化您的控制器。那么解决方法如下:
myTemplate.htlm:
我的模板.html:
<div>
<div>Hello</div>
<div ng-if="some-condition" ng-controller="Controller" ng-init="init(A, B, C)">
<div ng-include="'myTemplate.html'"></div>
</div>
</div>
I found by plain conincidence that this kind of structure can be made recursive as you like in plain vanilla angular. Just follow this design pattern and you can use recursive UI-structures without any advanced compilation tinkering etc.
我发现通过简单的巧合,这种结构可以像你喜欢的那样在普通的 vanilla angular 中递归。只需遵循此设计模式,您就可以使用递归 UI 结构,而无需任何高级编译修补等。
Inside your controller:
在您的控制器内部:
$scope.init = function(A, B, C) {
// Do something with A, B, C
$scope.D = A + B; // D can be passed on to other controllers in myTemplate.html
}
The only downside I can see is the clunky syntax you have to put up with.
我能看到的唯一缺点是您必须忍受笨拙的语法。
回答by Oleksandr Knyga
You can use angular-recursion-injector for that: https://github.com/knyga/angular-recursion-injector
您可以为此使用 angular-recursion-injector:https: //github.com/knyga/angular-recursion-injector
Allows you to do unlimited depth nesting with conditioning. Does recompilation only if needed and compiles only right elements. No magic in code.
允许您通过调节进行无限深度的嵌套。仅在需要时才重新编译并仅编译正确的元素。代码中没有魔法。
<div class="node">
<span>{{name}}</span>
<node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>
One of the things that allows it to work faster and simpler then the other solutions is "--recursion" suffix.
允许它比其他解决方案更快、更简单地工作的一件事是“--recursion”后缀。
回答by Jens
I ended up creating a set of basic directives for recursion.
我最终为递归创建了一组基本指令。
IMO It is far more basic than the solution found here, and just as flexible if not more, so we are not bound to using UL/LI structures etc... But obviously those make sense to use, however the directives are unaware of this fact...
IMO 它比这里找到的解决方案要基本得多,而且即使不是更多,也同样灵活,因此我们不必使用 UL/LI 结构等......但显然这些使用起来很有意义,但是指令不知道这一点事实...
A Super simple example would be:
一个超级简单的例子是:
<ul dx-start-with="rootNode">
<li ng-repeat="node in $dxPrior.nodes">
{{ node.name }}
<ul dx-connect="node"/>
</li>
</ul>
The implementation of 'dx-start-with' an 'dx-connect' is found at: https://github.com/dotJEM/angular-tree
“dx-start-with”和“dx-connect”的实现见:https: //github.com/dotJEM/angular-tree
This means you don't have to create 8 directives if you need 8 different layouts.
这意味着如果您需要 8 个不同的布局,则不必创建 8 个指令。
To create a tree-view on top of that where you can add or delete nodes would then be rather simple. As in: http://codepen.io/anon/pen/BjXGbY?editors=1010
在上面创建一个树视图,您可以在其中添加或删除节点,这将是相当简单的。如:http: //codepen.io/anon/pen/BjXGbY?editors=1010
angular
.module('demo', ['dotjem.angular.tree'])
.controller('AppController', function($window) {
this.rootNode = {
name: 'root node',
children: [{
name: 'child'
}]
};
this.addNode = function(parent) {
var name = $window.prompt("Node name: ", "node name here");
parent.children = parent.children || [];
parent.children.push({
name: name
});
}
this.removeNode = function(parent, child) {
var index = parent.children.indexOf(child);
if (index > -1) {
parent.children.splice(index, 1);
}
}
});
<div ng-app="demo" ng-controller="AppController as app">
HELLO TREE
<ul dx-start-with="app.rootNode">
<li><button ng-click="app.addNode($dxPrior)">Add</button></li>
<li ng-repeat="node in $dxPrior.children">
{{ node.name }}
<button ng-click="app.removeNode($dxPrior, node)">Remove</button>
<ul dx-connect="node" />
</li>
</ul>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.0/angular.min.js"></script>
<script src="https://rawgit.com/dotJEM/angular-tree-bower/master/dotjem-angular-tree.min.js"></script>
</div>
From this point on, the controller and template could be wrapped in it's own directive if one would wish for it.
从这一点开始,如果需要,控制器和模板可以包含在它自己的指令中。

