##背景
ファイル・フォルダの構造や、掲示板のスレッドなど、所謂木構造で表現されるデータ構造をAngularJSで取り扱うことを考える。
下記のようなデータ(ここではLinuxやWindowsのディレクトリ構造をイメージして欲しい)が与えられた場合に、このデータの階層構造を表示したい。
function MainCtrl($scope){
$scope.someTree = {
name: 'Root',
children: [{
name: 'dir1',
children: [
{name: 'file1'},
{name: 'file2'}
]
}, {
name: 'file3'
}]
};
}
ここでは、scopeのsomeTreeデータを表示するディレクティブ、"my-tree"を作成してみよう。
<div ng-controller="MainCtrl">
<div my-tree="someTree"></div>
</div>
my-treeに、scopeのデータをバインドさせることで、下記のように表示をさせるのだ。
- Root
- dir1
- file1
- file2
- file3
NGな例
まずは素朴にやってみよう。myTreeの名前でdirectiveを作成する。
myModule.directive('myTree', function(){
return{
restrict: 'A',
templateUrl: 'scripts/directives/templates/myTree.html',
scope: {
myTree: '='
}
};
});
ここでのポイントはtemplateUrlにて指定したテンプレートの中身である。再帰的に子を表示出来るよう、my-treeのテンプレート自身にmy-treeディレクティブが出現する。
<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毎に以下を実行していく.
- 要素に存在しているディレクティブのCompile
- 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ディレクティブである。
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);
});
};
}
};
}]);
<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 install angular-recursive
var myApp = angular.module('myApp', ['quramy-recursive']);
<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