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


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
Дякую, чудове рішення! по-справжньому чистим і відпрацьованим для мене, щоб зробити рекурсію між двома директивами, які включають один у одного роботу.
jssebastian

6
Первісна проблема полягає в тому, що при використанні рекурсивних директив AngularJS потрапляє в нескінченний цикл. Цей код розбиває цей цикл, видаляючи вміст під час події компіляції директиви та компілюючи та повторно додаючи вміст у події посилання директиви.
Марк Лагендійк

15
У вашому прикладі ви могли б замінити compile: function(element) { return RecursionHelper.compile(element); }з compile: RecursionHelper.compile.
Паоло Моретті

1
Що робити, якщо ви хочете, щоб шаблон знаходився у зовнішньому файлі?
CodyBugstein

2
Це елегантно в тому сенсі, що якщо / коли Angular core реалізує подібну підтримку, ви можете просто видалити спеціальну обгортку компіляції, і весь залишився код залишиться колишнім.
Карло Бонаміко

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

1
Я оновив ваш сценарій, щоб він мав лише одну директиву. jsfiddle.net/KNM4q/103 Як зробити так, щоб кнопка видалення працювала?
Бенні Боттема

Дуже хороша! Я був дуже близький, але не мав @position (я думав, що зможу знайти його з parentData [val]. Якщо ви оновите свою відповідь остаточною версією ( jsfiddle.net/KNM4q/111 ), я прийму її.
Benny Bottema

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директиву, а потім обернути її навколо елемента, який робить рекурсивний виклик.


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
@Jack Дякую, що вказав на це. Просто витратьте кілька годин на те, щоб зняти цю проблему, і ваш коментар вказав на мене в правильному напрямку. Для користувачів ASP.NET, що користуються послугою пакетного зв’язку, переконайтеся, що у вас немає старої мінімізованої версії файлу в каталозі, а ви використовуєте підстановку, що включає в комплект.
гравці

Для мене потрібен елемент для додавання всередину зворотного виклику, наприклад:. compiledContents(scope,function(clone) { iElement.append(clone); });Інакше контролер "вимагати" не правильно обробляється, і помилка: Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!причина.
Цунео Йосіока

Я намагаюся створити структуру дерева з кутовим js, але застряг із цим.
Заучування-переосмислений-плутаний

10

Щодо кутового 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>


1
Дякую за це Чи можете ви зв’язати мене із журналом змін, який запровадив цю функцію? Дякую!
Стівен

Використання кутового 1.5.x дуже важливо. 1.4.x не працюватиме, і це фактично версія, надана у jsfiddle.
Пакман

у jsfiddle jsfiddle.net/cattails27/5j5au76c немає того самого коду цієї відповіді ... це правильно? чого мені не вистачає?
Паоло Біавати

Загадка показує для кутових версій менше 1,5x
jkris

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>


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


2

Для цього існує дійсно дуже просте рішення, яке зовсім не вимагає директив.

Ну, в цьому сенсі, можливо, це навіть не рішення вихідної проблеми, якщо ви вважаєте, що вам потрібні директиви, але це рішення, якщо ви хочете рекурсивну структуру графічного інтерфейсу з параметризованими підструктурами графічного інтерфейсу. Що, мабуть, ви хочете.

Рішення засноване на просто використанні ng-контролера, 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
} 

Єдиний недолік, який я можу бачити, - це незграбний синтаксис, з яким ти повинен миритися.


Я боюся, що це не вдається вирішити проблему досить фундаментальним чином: При такому підході вам потрібно буде знати глибину рекурсії вперед, щоб мати достатню кількість контролерів у 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>

Однією з речей, яка дозволяє йому працювати швидше і простіше, ніж інші рішення, є суфікс "- рекурсія".


0

Я в кінцевому підсумку створив набір основних директив щодо рекурсії.

ІМО Це набагато основніше, ніж знайдене тут рішення, і настільки ж гнучко, якщо не більше, тому ми не зобов’язані використовувати структури 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>

З цього моменту контролер і шаблон можуть бути обгорнуті власною директивою, якщо цього хотіли б.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.