javascript - sistemas - Recursividad en directivas angulares
recursividad ejemplos resueltos (9)
Hay un par de frecuentes preguntas y respuestas sobre directivas recursivas recíprocas, que se reducen a una de las siguientes soluciones:
- "compilar" HTML de forma progresiva en función del estado del alcance del tiempo de ejecución
- no use una directiva, sino una plantilla <script> que se refiera a sí misma
El primero tiene el problema de que no puede eliminar el código previamente compilado a menos que administre de manera comprensible el proceso de compilación manual. El segundo enfoque tiene el problema de ... no ser una directiva y perder sus poderosas capacidades, pero con más urgencia, no se puede parametrizar de la misma manera que una directiva; simplemente está vinculado a una nueva instancia de controlador.
He estado jugando manualmente haciendo un angular.bootstrap
o @compile()
en la función de enlace, pero eso me deja con el problema de hacer un seguimiento de los elementos manualmente para eliminar y agregar.
¿Hay una buena manera de tener un patrón recursivo parametrizado que gestione la adición / eliminación de elementos para reflejar el estado del tiempo de ejecución? Es decir, un árbol con un botón de agregar / eliminar nodo y un campo de entrada cuyo valor se transmite a los nodos secundarios de un nodo. ¿Quizás una combinación del segundo enfoque con alcances encadenados (pero no tengo ni idea de cómo hacer esto)?
A partir de Angular 1.5.x, no se requieren más trucos, lo siguiente ha sido posible. No más necesidad de trabajo sucio alrededor!
Este descubrimiento fue producto de mi búsqueda de una solución mejor / limpia para una directiva recursiva. Puede encontrarlo aquí https://jsfiddle.net/cattails27/5j5au76c/ . Es compatible hasta ahora con 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>
Agregar elementos manualmente y compilarlos definitivamente es un enfoque perfecto. Si usa ng-repeat, entonces no tendrá que eliminar elementos manualmente.
Demostración: 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);
}
}
});
Ahora que Angular 2.0 está en la vista previa, creo que está bien agregar una alternativa angular 2.0 a la mezcla. Al menos beneficiará a la gente más tarde:
El concepto clave es construir una plantilla recursiva con una autorreferencia:
<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 continuación, enlaza un objeto de árbol con la plantilla y observa cómo la recursión se ocupa del resto. Aquí hay un ejemplo completo: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0
Después de usar varias soluciones por un tiempo, repetidamente vuelvo sobre este tema.
La solución de servicio no me satisface, ya que funciona para directivas que pueden inyectar el servicio pero no para fragmentos de plantilla anónimos.
Del mismo modo, las soluciones que dependen de la estructura de plantilla específica haciendo manipulación DOM en la directiva son demasiado específicas y frágiles.
Tengo lo que creo que es una solución genérica que encapsula la recursividad como una directiva propia que interfiere mínimamente con otras directivas y puede usarse de forma anónima.
A continuación hay una demostración con la que también puedes jugar en 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>
Hay una solución realmente muy simple para esto que no requiere directivas en absoluto.
Bueno, en ese sentido, tal vez ni siquiera sea una solución del problema original si supone que necesita directivas, pero ES una solución si desea una estructura de GUI recursiva con subestructuras estructuradas paramétricas de la GUI. Que es probablemente lo que quieres
La solución se basa solo en usar ng-controller, ng-init y ng-include. Simplemente hazlo de la siguiente manera, asume que tu controlador se llama "MyController", tu plantilla está ubicada en myTemplate.html y que tienes una función de inicialización en tu controlador llamada init que toma los argumentos A, B y C, lo que hace posible parametrice su controlador. Entonces la solución es la siguiente:
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>
Encontré por coincidencia que este tipo de estructura puede ser recursiva como prefiera en simple vainilla angular. Simplemente siga este patrón de diseño y puede usar estructuras de UI recursivas sin ningún retoque avanzado de compilación, etc.
Dentro de tu controlador:
$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
}
El único inconveniente que puedo ver es la sintaxis torpe que tienes que aguantar.
Inspirado por las soluciones descritas en el hilo mencionado por @ dnc253, resumí la funcionalidad de recursión en un servicio .
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);
}
}
};
}
};
}]);
Que se usa de la siguiente manera:
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);
}
};
}]);
Vea este Plunker para una demostración. Me gusta esta solución mejor porque:
- No necesita una directiva especial que hace que su html sea menos limpio.
- La lógica de recursión se abstrae en el servicio RecursionHelper, por lo que mantiene sus directivas limpias.
No estoy seguro de si esta solución se encuentra en uno de los ejemplos que vinculó o en el mismo concepto básico, pero necesitaba una directiva recursiva, y encontré una solución excelente y fácil .
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() {
}
}
};
});
Debe crear la directiva recursive
y luego envolverla alrededor del elemento que realiza la llamada recursiva.
Puede usar un inyector de recursión angular para eso: https://github.com/knyga/angular-recursion-injector
Le permite hacer anidamiento ilimitado en profundidad con acondicionamiento. Recopila solo si es necesario y compila solo los elementos correctos. Sin magia en el código.
<div class="node">
<span>{{name}}</span>
<node--recursion recursion-if="subNode" ng-model="subNode"></node--recursion>
</div>
Una de las cosas que le permite trabajar más rápido y más simple que las otras soluciones es el sufijo "--recisión".
Terminé creando un conjunto de directivas básicas para la recursión.
IMO Es mucho más básico que la solución que se encuentra aquí, y tan flexible, si no más, por lo que no estamos obligados a usar estructuras UL / LI, etc. ... Pero obviamente tiene sentido usarlas, sin embargo, las directivas no son conscientes de esto. hecho...
Un ejemplo super simple sería:
<ul dx-start-with="rootNode">
<li ng-repeat="node in $dxPrior.nodes">
{{ node.name }}
<ul dx-connect="node"/>
</li>
</ul>
La implementación de ''dx-start-with'' y ''dx-connect'' se encuentra en: https://github.com/dotJEM/angular-tree
Esto significa que no tiene que crear 8 directivas si necesita 8 diseños diferentes.
Para crear una vista de árbol en la parte superior de donde puede agregar o eliminar nodos sería bastante simple. Como en: 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>
A partir de este punto, el controlador y la plantilla podrían incluirse en su propia directiva si así lo desea.