Angularディレクティブの再帰


178

いくつかの人気のある再帰的な角度指令Q&Aがそこにあり、それらはすべて次の解決策の1つになります。

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

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

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

回答:


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ではなく、templateでのみ機能します


3
ありがとう、素晴らしい解決策!本当にクリーンで、箱から出して、お互いを含む2つのディレクティブ間で再帰的に機能するようにしました。
jssebastian 2013

6
元の問題は、再帰的なディレクティブを使用すると、AngularJSが無限ループに陥ることです。このコードは、ディレクティブのコンパイルイベント中にコンテンツを削除し、ディレクティブのリンクイベントのコンテンツをコンパイルして再度追加することにより、このループを解消します。
Mark Lagendijk 2013年

15
あなたの例では、置き換えることができcompile: function(element) { return RecursionHelper.compile(element); }compile: RecursionHelper.compile
パオロモレッティ

1
テンプレートを外部ファイルに配置したい場合はどうなりますか?
CodyBugstein 2015年

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

1
ディレクティブが1つだけになるようにスクリプトを更新しました。jsfiddle.net/KNM4q/103削除ボタンを機能させるにはどうすればよいですか?
ベニーボッテマ2013年

非常に素晴らしい!私は非常に近かったが、(私はparentData [ヴァル]でそれを見つけることができると思ったあなたは(最終版であなたの答えを更新した場合@positionを持っていませんでした。jsfiddle.net/KNM4q/111を)私はそれを受け入れるだろう。
ベニーボッテマ2013年

12

このソリューションがリンクした例の1つにあるのか、同じ基本概念にあるのかはわかりませんが、再帰的なディレクティブが必要でしたが、すばらしい簡単なソリューションが見つかりまし

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ファイルを2回以上含めていません。どういうわけか私のmain.jsファイルが2回含まれていたため、同じ名前の2つのディレクティブが作成されていました。JSインクルードの1つを削除した後、コードは機能しました。
ジャック

2
@ジャックそれを指摘してくれてありがとう。この問題のトラブルシューティングに何時間も費やすだけで、あなたのコメントが私を正しい方向に導きました。バンドリングサービスを使用するASP.NETユーザーの場合、バンドリングにワイルドカードインクルードを使用している間は、ディレクトリに古い縮小バージョンのファイルがないことを確認してください。
Beyers

:私にとっては、要素は同様に内部のコールバックを追加するために必要とされるcompiledContents(scope,function(clone) { iElement.append(clone); });、.Otherwise編コントローラが正しく処理されていない「必要」、およびエラー:Error: [$compile:ctreq] Controller 'tree', required by directive 'subTreeDirective', can't be found!原因。
吉岡恒夫2015年

私は角度のある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>


1
これをありがとう。この機能を導入した変更ログにリンクしていただけませんか?ありがとう!
スティーブン

角度1.5.xを使用することは非常に重要です。1.4.xは機能せず、jsfiddleで提供されるバージョンです。
Paqman 2016

jsfiddle jsfiddle.net/cattails27/5j5au76c には、この回答と同じコードはありません...正しいですか?何が欠けていますか?
Paolo Biavati

フィドルは
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>


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

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

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

このソリューションは、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
} 

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


これはかなり基本的な方法で問題を解決できないと思います。このアプローチでは、myTemplate.htmlに十分なコントローラーを配置するために、再帰の深さを事前に知る必要があります
Stewart_R

実際にはそうではありません。ファイルmyTemplate.htmlには、ng-includeを使用したmyTemplate.htmlへの自己参照が含まれているため(上​​記のhtmlコンテンツはmyTemplate.htmlのコンテンツであり、おそらく明確に記述されていません)。そうすれば、それは本当に再帰的になります。私はこの技法を生産で使用しました。
erobwen

また、おそらく明確に述べられていないのは、再帰を終了するためにng-ifをどこかで使用する必要があるということです。したがって、myTemplate.htmlは、私のコメントで更新された形式になります。
erobwen 2017年

0

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

他のソリューションよりも速くて簡単に機能するための1つの方法は、「-再帰」サフィックスです。


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>

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.