Angular.js レンダリング処理をカスタマイズする

Angular.jsを使っていてこんな風に思ったことはないだろうか。

* ループ内のDOMをテンプレート化したいな
* レンダリング処理を部品化してカスタムタグで呼び出したい
* jQuery UIとかを使いたいけど、どこで処理すればいいのか(document.readyだと新規追加したものに適用されない)

そんな願いをかなえるのがAngular.jsのdirectiveです。directiveは、一言で言えば  **ModelとDOMを受け取ってレンダリング処理(compile)を行う関数**  で、これを使って上記のようなカスタマイズをすることができます。

そんなわけで、作成したサンプルは[こちら](http://jsfiddle.net/icoxfog417/uvyaD/)。このコードを使って解説をしていきたいと思います。

## テンプレート化する
テンプレート化は、以下のようにかけます。
`<disp-destination ></disp-destination>`、これがdirectiveを呼び出しレンダリングしている箇所です。何このタグ？というのは次のセクションで語るとして、ここではscriptタグで宣言したテンプレートをdirectiveでどう使っているのかを解説します。

```html
・・・
    <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を渡しています。これでテンプレートによるレンダリングが行われるのだ。

```js
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(デフォルト)  | 属性として指定： &lt;div custom-directive&gt;&lt;/div&gt; |
| E  | タグとして指定： &lt;custom-directive&gt;&lt;/custom-directive&gt; |
| C  | クラスとして指定： &lt;div class="custom-directive"&gt;&lt;/div&gt; |
| M  | コメントとして指定： &lt;!-- directive: custom-directive --&gt; |

この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かなと思ったがうまくいかなかった。他にいいやり方があったら教えてください。。。

```js
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におけるレンダリング処理のカスタマイズ方法です。ここを知っていると大分活用の幅が広がるので、ぜひお試しされたし！
公式ドキュメントは[こちら](http://docs.angularjs.org/guide/directive)。英語だが、結構丁寧でわかりやすい。