Help us understand the problem. What is going on with this article?

AngularJSで再帰的なディレクティブを作成して木構造を表現する.

More than 5 years have passed since last update.

背景

ファイル・フォルダの構造や、掲示板のスレッドなど、所謂木構造で表現されるデータ構造をAngularJSで取り扱うことを考える。

下記のようなデータ(ここではLinuxやWindowsのディレクトリ構造をイメージして欲しい)が与えられた場合に、このデータの階層構造を表示したい。

Controller.js
function MainCtrl($scope){
    $scope.someTree = {                                    
      name: 'Root',                                        
      children: [{                                         
        name: 'dir1',                                      
        children: [
          {name: 'file1'}, 
          {name: 'file2'}
        ]       
      }, {                                                 
        name: 'file3'                                      
      }]                                                   
    };
}                                    

ここでは、scopeのsomeTreeデータを表示するディレクティブ、"my-tree"を作成してみよう。

index.html
<div ng-controller="MainCtrl">
  <div my-tree="someTree"></div>
</div>

my-treeに、scopeのデータをバインドさせることで、下記のように表示をさせるのだ。

  • Root
    • dir1
    •  file1
    •  file2
    • file3

NGな例

まずは素朴にやってみよう。myTreeの名前でdirectiveを作成する。

myTree.js
myModule.directive('myTree', function(){                         
  return{                                                           
    restrict: 'A',                                                  
    templateUrl: 'scripts/directives/templates/myTree.html',     
    scope: {                                                        
      myTree: '='                                                   
    }                                                               
  };                                                                
});                                                                 

ここでのポイントはtemplateUrlにて指定したテンプレートの中身である。再帰的に子を表示出来るよう、my-treeのテンプレート自身にmy-treeディレクティブが出現する。

scripts/directives/templates/myTree.html
<div>                                    
  <span>{{myTree.name}}</span>           
  <ul>                                   
    <li ng-repeat="node in myTree.children">
    <div my-tree="node"></div>       
    </li>                                
  </ul>                                  
</div>                                   

ちなみに、上記のmyTreeディレクティブは不良品である。このディレクティブをviewに配置したが最後、ブラウザが固まってしまうので要注意である。

原因は、myTreeのテンプレートに再帰的にmyTreeが出現している点である。ここで、「最終的には、myTreeのscopeは、終端のノードにたどり着くのだから、<li ng-repeat="node in myTree.children>が空になるのだから、問題ないのでは?」と考えたそこの貴方。
貴方はAngularのディレクティブにおける Compile, Link の違いを理解出来ていない。

AngularJSはViewのDOMをトラバースして、Element毎に以下を実行していく.

  1. 要素に存在しているディレクティブのCompile
  2. scopeの要素とDOMのバインド(これを"Link"という)

Compile段階では、要素に記載されたディレクティブのcompile関数が実行するのみで、scopeの値との紐付けはlink関数実行時である。したがって、myTreeディレクティブのテンプレートにmyTreeディレクティブが出現すれば、myTreeディレクティブのcompileは無限ループに突入してしまうのだ。

ちなみに、Angularのディレクティブを作成する際、一番冗長な記法となるのは下記である:

myModule.directive('myDirective', function(){
  return {
    // templateUrl等は割愛

    compile: function(tElement, tAttr){
      return {
        pre: function preLink(scope, element, attrs){
        },
        post: function postLink(scope, element, attrs){
        }
      };
    }
  };
});

見て分かるとおり、Compile関数の基本的な役割はLink関数(preLinkとpostLinkの2種類)を返却することである。

ちゃんと動く版

さて、若干話がそれてしまったが、myTreeディレクティブを完成させるには、どうすればよいのか?
勿体ぶっても仕方がないので、とっとと答えをお見せしよう。下記が修正版のmyTreeディレクティブである。

myTree.js
myModule.directive('myTree', ['$compile', function($compile){
  return{
    restrict: 'A',
    templateUrl: 'scripts/directives/templates/myTree.html',
    scope: {
      myTree: '='
    },
    compile: function(){
      // ここで再帰部分の子要素用テンプレートを作成.
      var childTemplate = '<li ng-repeat="node in myTree.children"><div my-tree="node"></div></li>';
      var childLinkFn; // キャッシュ用
      return function postLink(scope, element){
        // 一度だけ$compileを実行し、compileのクロージャにキャッシュ.
        // 子要素以降の再帰要素では、キャッシュされたLink関数を利用する.
        childLinkFn =  childLinkFn || $compile(childTemplate);

        // link関数を利用してscopeと紐付け.
        childLinkFn(scope, function(clonedElm){
          element.find('ul').append(clonedElm);
        });
      };
    }
  };
}]);                                                                    
scripts/directives/templates/myTree.html
<div>                          
  <span>{{myTree.name}}</span> 
  <ul></ul>                    
</div>                         

ポイントは $compile サービスをpostLink関数の中で実行している点である。$compile サービスは、先述したCompile -> Linkを実行するためのサービスであり、DOM要素を引数として、Link関数(コード中の childLinkFn)を返却させる効果がある。
myTreeディレクティブにおいては、テンプレートからmyTree自身の呼び出しを止め、代わりにLink関数の実行時に子ノードであるmyTreeのcompileを遅延実行することで、無限ループに陥ることなく、木構造を表現することが出来る。

とはいえ、AngularJSで、CompileとLinkの2フェーズに分かれていたものを無理やり1つのフェーズに押し込めているとも言える訳で、性能的に問題が出そうなのが怖いところだ

※ 2014/07/19 追記
コード修正. $compile の結果である子要素用のLink関数をcompileスコープでキャッシュするようにした。
木構造のscopeのルート要素に対するLink関数の実施時のみ、$compileが動作し、以降は通常のディレクティブと同じく、compileフェーズでリンク関数が作成されているのと同様の振る舞いとなるため、性能的にも問題ないはず.

自作ディレクティブの宣伝(2014.07追記)

拙作、angular-recursiveにて、汎用再帰データ用ディレクティブを公開しています. こちらも興味があれば使ってみてください.

bowerによるインストール
$ bower install angular-recursive
モジュールのロード
var myApp = angular.module('myApp', ['quramy-recursive']);
main.html
<div ng-controller="MainCtrl">
  <!-- q-recurseとq-recurse-nodeに囲まれたDOMが再帰レンダリングされる -->
  <div q-recurse="someTree">
    <span>{{someTree.name}}</span>
    <ul>
      <li ng-repeat="child in someTree.children">
        <div q-recurse-node="child"></div>
      </li>
    </ul>
  </div>
</div>

参考

compile, linkについてはAngularのドキュメントである下記が一番参考になる。

Quramy
Front-end web developer. TypeScript, Angular and Vim, weapon of choice.
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした