web-dev-qa-db-ja.com

Angularディレクティブの再帰

人気のある再帰的なangularディレクティブのQ&Aがいくつかあり、それらはすべて次のいずれかの解決策になります。

最初のには、手動のコンパイルプロセスを包括的に管理しない限り、以前にコンパイルされたコードを削除できないという問題があります。 2番目のアプローチには...ディレクティブではなく、その強力な機能を逃していないという問題がありますが、より緊急には、パラメータ化することはできませんディレクティブと同じ方法。単に新しいコントローラーインスタンスにバインドされます。

リンク関数でangular.bootstrapまたは@compile()を手動で実行することで遊んでいますが、削除および追加する要素を手動で追跡する問題が残ります。

ランタイム状態を反映するために要素の追加/削除を管理するパラメータ化された再帰パターンを持つ良い方法はありますか?つまり、ノードの追加/削除ボタンと、その値がノードの子ノードに渡される入力フィールドを持つツリーです。おそらく、2番目のアプローチと連鎖スコープとの組み合わせ(ただし、これを行う方法がわかりません)。

175
Benny Bottema

@ dnc253で言及されたスレッドで説明されているソリューションに触発されて、再帰機能 into a 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);
                    }
                }
            };
        }
    };
}]);

次のように使用されます:

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以降、これ以上のトリックは必要ありませんが、templateのみで機能し、templateUrl

313
Mark Lagendijk

手動で要素を追加してコンパイルすることは、間違いなく完璧なアプローチです。 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);
    }
}
});
23
SunnyShah

この解決策がリンクした例のいずれかまたは同じ基本概念にあるかどうかはわかりませんが、再帰的なディレクティブが必要でした 素晴らしい、簡単な解決策

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ディレクティブを作成してから、再帰呼び出しを行う要素にラップする必要があります。

12
dnc253

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

しばらくの間いくつかの回避策を使用した後、私は繰り返しこの問題に戻ってきました。

サービスをインジェクションできるディレクティブに対しては機能しますが、匿名テンプレートフラグメントに対しては機能しないため、サービスソリューションに満足していません。

同様に、ディレクティブで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="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

これには、ディレクティブをまったく必要としない、本当に簡単な回避策があります。

まあ、その意味では、おそらくディレクティブが必要だと仮定した場合、元の問題の解決策でさえないかもしれませんが、GUIのパラメータ化されたサブ構造を持つ再帰的なGUI構造が必要な場合はIS解決策です。これはおそらくあなたが望むものです。

ソリューションは、ng-controller、ng-init、およびng-includeを使用することに基づいています。コントローラーを「MyController」と呼び、テンプレートがmyTemplate.htmlにあり、コントローラーに引数A、B、およびCを受け取るinitという初期化関数があり、コントローラーをパラメーター化します。次に、解決策は次のとおりです。

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
} 

私が見ることができる唯一の欠点は、我慢しなければならない不格好な構文です。

2
erobwen

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.

2
TGH

そのためにangle-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>

他のソリューションよりも速く簡単に動作できるようにするものの1つは、「-recursion」サフィックスです。

0
Oleksandr Knyga

私は再帰のための一連の基本的なディレクティブを作成することになりました。

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

この時点から、コントローラーとテンプレートは、必要に応じて独自のディレクティブでラップできます。

0
Jens