Рекурсия в угловых директивах

178

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

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

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

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

Бенни Боттема
источник

Ответы:

316

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

Марк Лагендейк
источник
3
Спасибо, отличное решение! действительно чистый и разработанный для меня, чтобы сделать рекурсию между двумя директивами, которые включают в себя работу друг друга.
Джессебастьян
6
Первоначальная проблема заключается в том, что при использовании рекурсивных директив AngularJS попадает в бесконечный цикл. Этот код прерывает этот цикл, удаляя содержимое во время события компиляции директивы, а также компилируя и повторно добавляя содержимое в событие ссылки директивы.
Марк Лагендейк
15
В вашем примере вы можете заменить compile: function(element) { return RecursionHelper.compile(element); }на compile: RecursionHelper.compile.
Паоло Моретти
1
Что если вы хотите, чтобы шаблон находился во внешнем файле?
CodyBugstein
2
Это элегантно в том смысле, что если / когда ядро ​​Angular реализует аналогичную поддержку, вы можете просто удалить пользовательскую оболочку компиляции, и весь оставшийся код останется прежним.
Карло Бонамико
25

Добавление элементов вручную и их компиляция - безусловно, идеальный подход. Если вы используете 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);
    }
}
});
SunnyShah
источник
1
Я обновил ваш скрипт так, чтобы он имел только одну директиву. jsfiddle.net/KNM4q/103 Как заставить эту кнопку удаления работать?
Бенни Боттема
Очень хорошо! Я был очень близок, но у меня не было @position (я думал, что смогу найти его с parentData [val]. Если вы обновите свой ответ с помощью окончательной версии ( jsfiddle.net/KNM4q/111 ), я приму его.
Бенни Боттема
12

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

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

dnc253
источник
1
@MarkError и @ dnc253 это полезно, однако я всегда получаю следующую ошибку:[$compile:multidir] Multiple directives [tree, tree] asking for new/isolated scope on: <recursive tree="tree">
Джек,
1
Если кто-то еще сталкивается с этой ошибкой, только вы (или Yoeman) не включали файлы JavaScript более одного раза. Каким-то образом мой файл main.js был включен дважды, и поэтому были созданы две директивы с одинаковыми именами. После удаления одного из JS включает, код работал.
Джек,
2
@ Джек Спасибо за указание на это. Просто потратьте несколько часов на решение этой проблемы, и ваш комментарий указал мне правильное направление. Для пользователей ASP.NET, использующих службу пакетирования, убедитесь, что у вас нет старой минимизированной версии файла в каталоге, когда вы используете подстановочные знаки, включенные в пакетирование.
Бейерс
Для меня, элемент необходим для добавления внутри обратного вызова, например:. compiledContents(scope,function(clone) { iElement.append(clone); });Иначе, «require» ed контроллер не обрабатывается правильно, и error: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!причина.
Цунео Йошиока
Я пытаюсь создать древовидную структуру с угловой JS, но застрял с этим.
Обученный-Смущенный-Смущенный
10

Начиная с версии 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>

jkris
источник
1
Спасибо за это. Не могли бы вы связать меня с журналом изменений, который представил эту функцию? Спасибо!
Стивен
Использование угловых 1.5.x очень важно. 1.4.x не будет работать и является версией, представленной в jsfiddle.
Пакман
в jsfiddle jsfiddle.net/cattails27/5j5au76c нет того же самого кода этого ответа ... это правильно? чего мне не хватает?
Паоло
Скрипка показывает для угловых версий менее
1,5x
4

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

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

Точно так же решения, которые зависят от конкретной структуры шаблона путем выполнения 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="angular.js@1.3.15" 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>

tilgovi
источник
2

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

TGH
источник
2

Для этого есть действительно очень простой обходной путь, который вообще не требует директив.

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

Решение основано только на использовании 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>

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

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

erobwen
источник
Боюсь, что это не решит проблему довольно фундаментальным способом: при таком подходе вам нужно будет знать глубину рекурсии заранее, чтобы иметь достаточно контроллеров в myTemplate.html
Stewart_R
На самом деле, вы не делаете. Так как ваш файл myTemplate.html содержит собственную ссылку на myTemplate.html с использованием ng-include (вышеупомянутое html-содержимое - это содержимое myTemplate.html, возможно, это неясно указано). Таким образом, это становится действительно рекурсивным. Я использовал технику в производстве.
erobwen
Кроме того, возможно, неясно указано, что вам также нужно использовать ng-if где-нибудь для завершения рекурсии. Таким образом, ваш myTemplate.html имеет форму, обновленную в моем комментарии.
erobwen
0

Для этого вы можете использовать угловой рекурсивный инжектор: 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".

Александр Кныга
источник
0

Я закончил тем, что создал набор основных директив для рекурсии.

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

С этого момента контроллер и шаблон могут быть обернуты в свою собственную директиву, если захотите.

Jens
источник