Angular.jsを使っていてこんな風に思ったことはないだろうか。
- ループ内のDOMをテンプレート化したいな
- レンダリング処理を部品化してカスタムタグで呼び出したい
- jQuery UIとかを使いたいけど、どこで処理すればいいのか(document.readyだと新規追加したものに適用されない)
そんな願いをかなえるのがAngular.jsのdirectiveです。directiveは、一言で言えば ModelとDOMを受け取ってレンダリング処理(compile)を行う関数 で、これを使って上記のようなカスタマイズをすることができます。
そんなわけで、作成したサンプルはこちら。このコードを使って解説をしていきたいと思います。
テンプレート化する
テンプレート化は、以下のようにかけます。
<disp-destination ></disp-destination>
、これがdirectiveを呼び出しレンダリングしている箇所です。何このタグ?というのは次のセクションで語るとして、ここではscriptタグで宣言したテンプレートをdirectiveでどう使っているのかを解説します。
・・・
<div id="itemList">
<div class="item" ng-repeat="d in destinations" >
<disp-destination ></disp-destination><!-- directiveによるレンダリング -->
</div>
</div>
・・・
<script type="text/html" id="template"><!-- こちらがテンプレート -->
{{d.name}}:{{d.address}}
<div class="complete" style="font-size:8px" range-select ></div>
</script>
こちらがdirectiveの宣言。moduleにdispDestination
を追加しています。directiveではテンプレートを返すことができ、ここでjQueryで取得したテンプレートのhtml DOMを渡しています。これでテンプレートによるレンダリングが行われるのだ。
var santaApp = angular.module("santaApp",[]);
・・・
santaApp.directive("dispDestination",function(){
//テンプレートをid指定で取得、htmlを設定
return {
restrict: 'E',
template: $("#template").html()
};
})
そしてお気づきかもしれないが、directiveは定義するときはキャメルケースだが使うときはハイフンつなぎになる(customDirectiveならcustom-directiveになる)。この点は要注意。
##カスタムタグを使いたい
先ほどのJavaScriptコードでrestrict 'E'
という記載がありましたが、これがキモ。restrictがdirectiveの指定方法を表しています(EだとElement、つまりタグで判断する)。
指定の方法はいくつかあり下記にまとめていますが、正直AかEしか使わないので他のrestrictについてはドキュメント上存在するが動作は未確認。。。
restrict | directiveの指定方法 |
---|---|
A(デフォルト) | 属性として指定: <div custom-directive></div> |
E | タグとして指定: <custom-directive></custom-directive> |
C | クラスとして指定: <div class="custom-directive"></div> |
M | コメントとして指定: <!-- directive: custom-directive --> |
このrestrictをうまく使用することで、モジュール化を行うとともに可読性の高いViewを作成できます。
レンダリングしたDOMを処理したい(jQuery UIなど)
今回はjQuery UI からSliderを使用しました。ポイントは以下2点。
1 directiveを使用し、DOM要素をSliderオブジェクトにする( $("xxx").slider()
を行う)
2 Sliderをスライドさせたイベントを検知し、scope内のModelに反映する
directiveでは以下の変数を受け取ることができ、elementがあれば①はもうクリア可能です。問題は②のイベントハンドリングで、ここはSlider側で検知したイベントからAngular内のscopeを更新する必要があります。
directiveで受け取れる変数
- scope:グローバルでなくローカルスコープなので注意。この場合、ループ中のスコープになるため d というループ中で使用している変数にアクセスできる
- element:directiveが付与されたDOM
- attr:指定された属性の情報
解決策として今回とったのは、Angularのscopeにあらかじめ値同期用の関数を用意しておき、それをイベント処理内で呼び出す方法です。
→ng-Modelを設定したフォーム項目を用意しておいてそこの値を変えればOKかなと思ったがうまくいかなかった。他にいいやり方があったら教えてください。。。
var santaApp = angular.module("santaApp",[]);
santaApp.controller("santaAppCtrl",function($scope){
$scope.title = "Santa Claus Delivery List";
$scope.destinations =
[new Destination("Mike","NewYork"),
new Destination("Bekky","Japan"),
new Destination("Bob","Africa")];
//受け取ったindex要素にcompleteの値を反映する関数
$scope.applyComplete = function(index,complete){
$scope.destinations[index].complete = complete;
}
})
santaApp.directive("rangeSelect",function(){
return function(scope, element, attrs){
$(element[0]).slider({
range: "min",
value: scope.d.complete,
min: 1,
max: 100,
slide: function( event, ui ) {
var index = $("#itemList .complete").index(this);
var scope = angular.element($("#delivery")).scope(); //グローバルスコープ取得
scope.applyComplete(index,ui.value); //slideイベントからscope内の値を更新
}
});
}
})
以上が、Angular.jsにおけるレンダリング処理のカスタマイズ方法です。ここを知っていると大分活用の幅が広がるので、ぜひお試しされたし!
公式ドキュメントはこちら。英語だが、結構丁寧でわかりやすい。