Рекурсия в директивах Angular

Есть пара популярных рекурсивных angular директив Q & A, которые все сводятся к одному из следующих решений:

В первой версии есть проблема, что вы не можете удалить ранее скомпилированный код, если вы не сможете с достаточной степенью точности управлять процессом компиляции вручную. Второй подход имеет проблему... не являющейся директивой и отсутствующей в ее мощных возможностях, но более срочно ее нельзя параметризовать так же, как может быть директива; он просто привязан к новому экземпляру контроллера.

Я играл с ручным выполнением angular.bootstrap или @compile() в функции ссылок, но это оставляет мне проблему с ручным отслеживанием элементов для удаления и добавления.

Есть ли хороший способ иметь параметризованный рекурсивный шаблон, который управляет добавлением/удалением элементов для отражения состояния времени выполнения? То есть дерево с кнопкой add/delete node и некоторое поле ввода, значение которого передается дочерними узлами node. Возможно, комбинация второго подхода с привязанными областями (но я не знаю, как это сделать)?

Ответ 1

Вдохновленный решениями, описанными в теме, упомянутой @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);
        }
    };
}]);

Посмотрите этот Plunker для демонстрации. Мне больше нравится это решение, потому что:

  1. Вам не нужна специальная директива, которая делает ваш HTML менее чистым.
  2. Логика рекурсии абстрагируется от службы RecursionHelper, поэтому вы сохраняете свои директивы в чистоте.

Обновление: Начиная с Angular 1.5.x, больше никаких трюков не требуется, но работает только с шаблоном, а не с templateUrl.

Ответ 2

Вручную добавлять элементы и компилировать их, безусловно, является идеальным подходом. Если вы используете 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);
    }
}
});

Ответ 3

Я не знаю наверняка, если это решение найдено в одном из приведенных вами примеров или одной и той же базовой концепции, но мне нужна рекурсивная директива, и я нашел отличное, простое решение.

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, а затем обернуть ее вокруг элемента, который делает рекурсивный вызов.

Ответ 4

Как и в случае с Angular 1.5.x, больше не требуется никаких трюков, стало возможным следующее. Больше нет необходимости в грязной работе!

Это открытие было результатом моей охоты на лучшее/более чистое решение для рекурсивной директивы. Вы можете найти его здесь https://jsfiddle.net/cattails27/5j5au76c/. Он поддерживает до 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>

Ответ 5

После использования нескольких обходных решений какое-то время я неоднократно возвращался к этой проблеме.

Я не удовлетворен решением службы, так как он работает для директив, которые могут внедрять службу, но не работают для анонимных фрагментов шаблона.

Аналогично, решения, которые зависят от конкретной структуры шаблона, делая манипуляции с 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="[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="[email protected]*"></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>

Ответ 6

Теперь, когда 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>

Затем вы привязываете объект дерева к шаблону и наблюдаете, как рекурсия заботится обо всем остальном. Вот полный пример: http://www.syntaxsuccess.com/viewarticle/recursive-treeview-in-angular-2.0

Ответ 7

Существует действительно очень простой способ обхода, который вообще не требует директив.

Ну, в этом смысле, возможно, это даже не решение исходной проблемы, если вы предполагаете, что вам нужны директивы, но это решение, если вам нужна рекурсивная структура графического интерфейса с параметризованными подструктурами графического интерфейса. Скорее всего, вы хотите.

Решение основано только на использовании ng-controller, ng-init и ng-include. Просто сделайте это следующим образом, предположите, что ваш контроллер называется 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>

Я понял, что такая структура может быть рекурсивной, как вам нравится в простой ваниле angular. Просто следуйте этому шаблону проектирования, и вы можете использовать рекурсивные 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
} 

Единственный недостаток, который я вижу, - это неуклюжий синтаксис, с которым вам приходится мириться.

Ответ 8

В итоге я создал набор основных директив для рекурсии.

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

Ответ 9

Вы можете использовать angular -recursion-injector для этого: https://github.com/knyga/angular-recursion-injector

Позволяет выполнять неограниченную глубину с помощью кондиционирования. Перекомпиляция только при необходимости и компиляция только правильных элементов. Никакой магии в коде.

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

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

Одна из вещей, которая позволяет ему работать быстрее и проще, тогда другие решения - суффикс "--recursion".