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

##背景

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


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

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

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

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

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

- Root
 - dir1
  - 　file1
  - 　file2
 - file3

## NGな例

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

```js:myTree.js
myModule.directive('myTree', function(){                         
  return{                                                           
    restrict: 'A',                                                  
    templateUrl: 'scripts/directives/templates/myTree.html',     
    scope: {                                                        
      myTree: '='                                                   
    }                                                               
  };                                                                
});                                                                 
```
ここでのポイントはtemplateUrlにて指定したテンプレートの中身である。再帰的に子を表示出来るよう、my-treeのテンプレート自身にmy-treeディレクティブが出現する。

```html: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のディレクティブを作成する際、一番冗長な記法となるのは下記である：

```js
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ディレクティブである。

```js: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);
        });
      };
    }
  };
}]);                                                                    
```

```html: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](https://github.com/Quramy/angular-recursive)にて、汎用再帰データ用ディレクティブを公開しています. こちらも興味があれば使ってみてください.

```bash:bowerによるインストール
$ bower install angular-recursive
```

```js:モジュールのロード
var myApp = angular.module('myApp', ['quramy-recursive']);
```

```html: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のドキュメントである下記が一番参考になる。

 - https://docs.angularjs.org/guide/compiler HTML Compilerガイド
 - https://docs.angularjs.org/api/ng/service/$compile \$compileサービスのAPI