Recursión en directivas angulares

Resuelto Benny Bottema asked hace 11 años • 9 respuestas

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.bootstrapo @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)?

Benny Bottema avatar Jan 21 '13 06:01 Benny Bottema
Aceptado

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:

  1. No necesita una directiva especial que haga que su html sea menos limpio.
  2. 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

Mark Lagendijk avatar Sep 04 '2013 09:09 Mark Lagendijk

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);
    }
}
});
SunnyShah avatar Jan 22 '2013 14:01 SunnyShah

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 recursivedirectiva y luego envolverla alrededor del elemento que realiza la llamada recursiva.

dnc253 avatar Feb 02 '2013 00:02 dnc253