web-dev-qa-db-fra.com

Récursion dans les directives Angular

Il y a quelques questions-réponses sur les directives récursives angular populaires, qui se résument toutes à l'une des solutions suivantes:

Le premier pose le problème suivant: vous ne pouvez pas supprimer le code précédemment compilé à moins de gérer de manière compréhensible le processus de compilation manuelle. La seconde approche pose le problème suivant: elle n'est pas une directive et manque de ses puissantes capacités, mais de manière plus urgente, elle ne peut pas être paramétrée. de la même manière qu'une directive peut être; il est simplement lié à une nouvelle instance de contrôleur.

J'ai joué manuellement avec un angular.bootstrap ou @compile() dans la fonction de liaison, mais cela me laisse le problème de garder manuellement la trace des éléments à supprimer et à ajouter.

Existe-t-il un bon moyen d'avoir un modèle récursif paramétré qui gère l'ajout/la suppression d'éléments pour refléter l'état d'exécution? C'est-à-dire, une arborescence avec un bouton ajouter/supprimer un noeud et un champ de saisie dont la valeur est transmise aux noeuds enfants d'un noeud. Peut-être une combinaison de la deuxième approche avec des portées chaînées (mais je ne sais pas comment faire cela)?

175
Benny Bottema

Inspiré par les solutions décrites dans le fil mentionné par @ dnc253, j'ai résumé la fonctionnalité de récursion dans un 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);
                    }
                }
            };
        }
    };
}]);

Qui est utilisé comme suit:

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

Voir ceci Plunker pour une démo. J'aime mieux cette solution parce que:

  1. Vous n'avez pas besoin d'une directive spéciale qui rend votre HTML moins propre.
  2. La logique de récurrence est abstraite dans le service RecursionHelper, de sorte que vos directives restent propres.

Mise à jour: à partir de Angular 1.5.x, aucune astuce supplémentaire n’est requise, mais fonctionne uniquement avec le modèle , pas avec templateUrl

313
Mark Lagendijk

Ajouter manuellement des éléments et les compiler est définitivement une approche parfaite. Si vous utilisez ng-repeat, vous ne serez pas obligé de supprimer manuellement les éléments.

Démo: 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);
    }
}
});
23
SunnyShah

Je ne sais pas avec certitude si cette solution se trouve dans l'un des exemples que vous avez liés ou dans le même concept de base, mais j'avais besoin d'une directive récursive et j'ai trouvé ne excellente solution simple .

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

Vous devez créer la directive recursive, puis l'enrouler autour de l'élément qui effectue l'appel récursif.

12
dnc253

À partir de Angular 1.5.x, plus aucune astuce n'est requise, les possibilités suivantes sont possibles. Plus besoin de travaux sales!

Cette découverte était un produit de ma quête d'une solution meilleure/plus propre pour une directive récursive. Vous pouvez le trouver ici https://jsfiddle.net/cattails27/5j5au76c/ . Il supporte autant que 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>
10
jkris

Après avoir utilisé plusieurs solutions de contournement pendant un certain temps, je suis revenu à plusieurs reprises sur ce problème.

Je ne suis pas satisfait de la solution de service, car elle fonctionne pour les directives pouvant injecter le service, mais ne fonctionne pas pour les fragments de modèles anonymes.

De même, les solutions qui dépendent d'une structure de modèle spécifique en effectuant une manipulation de DOM dans la directive sont trop spécifiques et fragiles.

J'ai ce que je crois être une solution générique qui résume la récursivité en tant que directive propre qui interfère de manière minimale avec toute autre directive et peut être utilisée de manière anonyme.

Vous trouverez ci-dessous une démonstration avec laquelle vous pouvez également jouer à 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>
4
tilgovi

Il existe une solution de contournement très simple pour cela qui ne nécessite aucune directive.

Eh bien, dans ce sens, ce n'est peut-être même pas une solution du problème initial si vous supposez que vous avez besoin de directives, mais une solution IS si vous souhaitez une structure d'interface graphique récursive avec des sous-structures paramétrées de l'interface graphique. C'est probablement ce que vous voulez.

La solution est basée sur l'utilisation de ng-controller, ng-init et ng-include. Procédez comme suit, en supposant que votre contrôleur s’appelle "MyController", que votre modèle se trouve dans myTemplate.html et que vous disposiez d’une fonction d’initialisation appelée init, qui prend les arguments A, B et C, permettant de paramétrez votre contrôleur. Alors la solution est la suivante:

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>

J'ai trouvé par pure conviction que ce genre de structure peut être rendue récursive à votre guise en plain Vanilla angular. Suivez simplement ce modèle et vous pouvez utiliser des structures d'interface utilisateur récursives sans bricolage de compilation avancé, etc.

Dans votre contrôleur:

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

Le seul inconvénient que je puisse constater est la syntaxe maladroite que vous devez accepter.

2
erobwen

Maintenant que Angular 2.0 est en aperçu, je pense qu’il est correct d’ajouter une alternative Angular 2.0 au mélange. Au moins, cela profitera aux gens plus tard:

Le concept clé est de construire un modèle récursif avec une auto-référence:

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

Vous liez ensuite un objet d'arbre au modèle et observez la récursion s'occuper du reste. Voici un exemple complet: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.

2
TGH

Vous pouvez utiliser angular-récursion-injecteur pour cela: https://github.com/knyga/angular-recursion-injector

Vous permet d'effectuer une imbrication de profondeur illimitée avec conditionnement. Ne recompilez que si nécessaire et ne compile que les bons éléments. Pas de magie dans le code.

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

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

Une des choses qui lui permet de travailler plus rapidement et plus facilement que les autres solutions est le suffixe "--recursion".

0
Oleksandr Knyga

J'ai fini par créer un ensemble de directives de base pour la récursivité.

IMO C'est beaucoup plus fondamental que la solution trouvée ici, et tout aussi flexible, sinon plus, de sorte que nous ne sommes pas tenus d'utiliser des structures UL/LI, etc. fait...

Un exemple très simple serait:

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

L'implémentation de 'dx-start-with' et d'un 'dx-connect' se trouve à l'adresse suivante: https://github.com/dotJEM/angular-tree

Cela signifie que vous n'avez pas à créer 8 directives si vous avez besoin de 8 présentations différentes.

Créer une arborescence au-dessus de celle où vous pouvez ajouter ou supprimer des nœuds serait alors plutôt simple. Comme dans: http://codepen.io/anon/pen/BjXGbY?editors=101

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>

À partir de ce moment, le contrôleur et le modèle pourraient être intégrés à sa propre directive si on le souhaitait.

0
Jens