角度指令递归

有一些流行的递归角度指令问答,它们都归结为以下解决方案之一:

第一个 有一个问题,除非您能够理解地管理手工编译过程,否则无法删除先前编译的代码。第二种方法有一个问题... ... 不是一个指令,错过了它的强大功能,但更紧迫的是,它不能像指令那样被参数化; 它只是绑定到一个新的控制器实例。

我一直在玩手动做一个 angular.bootstrap@compile()的链接函数,但这留给我的问题,手动保持跟踪的元素删除和添加。

有没有一种好的方法可以使用参数化的递归模式来管理添加/删除元素以反映运行时状态?也就是说,一个带有添加/删除节点按钮的树和一些输入字段,它们的值通过节点的子节点传递。也许第二种方法与链式作用域相结合(但我不知道如何做到这一点) ?

70331 次浏览

手动添加元素并编译它们绝对是一种完美的方法。如果使用 ng-repeat,则不必手动删除元素。

演示: 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);
}
}
});

我不确定这个解决方案是在你链接的例子中还是在相同的基本概念中找到的,但是我需要一个递归指令,我找到了 一个伟大的,简单的解决方案

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() {
}
}
};
});​

您应该创建 recursive指令,然后将其包围在进行递归调用的元素周围。

受到@dnc253提到的线程中描述的解决方案的启发,我抽象出了递归功能 变成了一种服务

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);
}
}
};
}
};
}]);

用法如下:

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);
}
};
}]);

请参见此 笨蛋演示。 我最喜欢这个解决方案,因为:

  1. 您不需要一个特殊的指令来使您的 html 不那么干净。
  2. 递归逻辑被抽象到 RecursionHelper 服务中,因此您可以保持指令的整洁。

更新: 到 Angular 1.5.x 为止,不需要更多的技巧,但只适用于 模板,而不适用于 < em > templateUrl

我最终创建了一组递归的基本指令。

IMO 它比我们在这里找到的解决方案要基本得多,并且如果不是更灵活的话也是一样的,所以我们不一定要使用 UL/LI 结构等等。.但显然,这些都是有意义的使用,然而,指令没有意识到这一事实..。

一个超级简单的例子是:

<ul dx-start-with="rootNode">
<li ng-repeat="node in $dxPrior.nodes">
\{\{ node.name }}
<ul dx-connect="node"/>
</li>
</ul>

“ dx-start-with”和“ dx-connect”的实现位于: https://github.com/dotJEM/angular-tree

这意味着如果需要8种不同的布局,就不必创建8个指令。

在此基础上创建一个可以添加或删除节点的树视图将非常简单。就是 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>

从这一点开始,如果需要的话,控制器和模板可以包装在它自己的指令中。

在使用了几个变通方法一段时间之后,我反复回到这个问题。

我对服务解决方案不满意,因为它适用于可以注入服务的指令,但不适用于匿名模板片段。

类似地,通过在指令中执行 DOM 操作而依赖于特定模板结构的解决方案过于具体和脆弱。

我相信我有一个通用的解决方案,它将递归封装为自己的指令,可以最小限度地干扰任何其他指令,并且可以匿名使用。

下面是一个演示,您也可以在 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="angular.js@1.3.15" 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>

现在 Angular 2.0已经发布了预览版,我认为可以添加一个 Angular 2.0的替代品。至少以后它会让人们受益:

关键概念是构建一个带有自引用的递归模板:

<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>

然后将一个树对象绑定到模板,并观察递归处理其余的事情。 下面是一个完整的例子: < a href = “ http://www.syntaxSuccess. com/viewararticle/recsive-treeview-in-angle-2.0”rel = “ nofollow”> http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

可以使用角递归注入器: https://github.com/knyga/angular-recursion-injector

允许您做无限深度嵌套与条件。仅在需要时进行重新编译,并仅编译正确的元素。密码里没有魔法。

<div class="node">
<span>\{\{name}}</span>


<node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>

允许它比其他解决方案工作得更快、更简单的一个因素是“——递归”后缀。

有一个非常简单的解决方案,根本不需要指令。

好吧,从这个意义上说,如果你假设你需要指令,也许它甚至不是最初问题的解决方案,但是如果你想要一个具有 GUI 的参数化子结构的递归 GUI 结构,它就是一个解决方案。这可能就是你想要的。

该解决方案基于只使用 n- 控制器、 n- 初始化和 n- 包含。只需按照以下步骤操作,假设您的控制器名为“ MyController”,您的模板位于 myTemplate.html,并且您的控制器上有一个名为 init 的初始化函数,该函数接受参数 A、 B 和 C,从而使参数化您的控制器成为可能。那么解决方案如下:

MyTemplate.htlm:

<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>

我发现这种结构可以用普通的香草角来递归。只要遵循这种设计模式,您就可以使用递归 UI 结构,而不需要任何高级编译修补等。

在你的控制器里:

$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
}

我能看到的唯一缺点是你不得不忍受的笨拙语法。

从 Angular 1.5.x 开始,不再需要更多的技巧,以下内容已经成为可能。再也不需要肮脏的工作了!

这个发现是我为递归指令寻找更好/更干净的解决方案的副产品。你可以在这里找到它。它只支持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>