Recursión en directivas angulares
Hay un par de preguntas y respuestas populares sobre directivas angulares recursivas, que se reducen a una de las siguientes soluciones:
- HTML 'compilar' manualmente de forma incremental según el estado del alcance del tiempo de ejecución
- ejemplo 1 [ desbordamiento de pila ]
- ejemplo 2 [ página de jsfiddles angulares ]
- No use ninguna directiva, sino una plantilla <script> que se refiere a sí misma.
- ejemplo 1 [ grupos de google ]
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 lo más urgente es que 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 una función angular.bootstrap
o @compile()
en la función de enlace, pero eso me deja con el problema de realizar un seguimiento manual de los elementos para eliminar y agregar.
¿Existe una buena manera de tener un patrón recursivo parametrizado que administre agregar/eliminar elementos para reflejar el estado de tiempo de ejecución? Es decir, un árbol con un botón para agregar/eliminar nodo y algún 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 idea de cómo hacerlo)?
Inspirándome en las soluciones descritas en el hilo mencionado por @dnc253, abstraí la funcionalidad de recursividad 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 utiliza 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 más esta solución porque:
- No necesita una directiva especial que haga que su html sea menos limpio.
- La lógica de recursividad se abstrae en el servicio RecursionHelper, por lo que mantiene limpias sus directivas.
Actualización: a partir de Angular 1.5.x, no se requieren más trucos, pero solo funciona con template , no con templateUrl
Agregar elementos manualmente y compilarlos es definitivamente un enfoque perfecto. Si usa ng-repeat, 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);
}
}
});
No sé con certeza 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 recursive
directiva y luego envolverla alrededor del elemento que realiza la llamada recursiva.