AngularJS1.4とbindToController


こんにちは。imaiです。待望のAngularJS1.4がリリースされましたね。Directive関係では、[`bindToController`](https://code.angularjs.org/1.4.0-rc.2/docs/api/ng/service/$compile)の機能が向上しました。
この`bindToController`とは一体何なのか。使って嬉しいことは何か。どう機能が向上したのか。について書いていきます。また、最後に自作Directiveを作っていく際に筆者がよくつまずく点を記しておきます。

この記事の対象者:AngularJSに触れたことがある、自作のDirectiveを作ったことがある
#bindToControllerとは
[AngularJS公式サイトのReference](https://code.angularjs.org/1.4.0-rc.2/docs/api/ng/service/$compile)から`bindToConroller`の説明を意訳すると、
>再利用可能なコンポーネントを作るために、分離?隔離?スコープや`controllerAs`を用いる際に、`bindToController:true`とすることでコンポーネントのプロパティは`controller`に結び付けられる(`scope`には結びつかない)。コントローラが生成していれば、分離スコープに結びついた値を利用することができる。

`bindToController`は、[コントローラー](https://code.angularjs.org/1.4.0-rc.2/docs/api/ng/service/$compile)と[分離スコープ](https://code.angularjs.org/1.4.0-rc.2/docs/api/ng/service/$compile)(他のスコープから切り離されたDirectiveが持てるスコープ)を結びつけると解釈していいと思います。
*"bind to controller" === "bind an isolated scope to a controller"*
ということ。
ざっくりいいますと、これまでDirectiveを自作する時に、コントローラーへ値を引き渡し、バインディングするには`$scope`が必要でしたが、これが`$scope`いらずで行えるようになりました。ng-controllerのas構文では先行して可能だったことがDirectiveでも可能になったんです。また、1.4では、どの値をバインディングするのか、どの値をバインディングしないのかを明確に定めることができるようになりました。

#bindToControllerを使って嬉しいこと、機能向上したところ
では実際に「`bindToController`を利用することで嬉しいことは何か」について書いていきます。例を示した方が早いと思うので、

- AngularJS 1.2.xを用いて自作Directiveを作る場合
- AngularJS 1.3.xを用いて自作Directiveを作る場合
- AngularJS 1.4.xを用いて自作Directiveを作る場合

この３つの例を見ていきます。お題は「ボタンがクリックされたら、クリックされた回数を表示する」
##AngularJS 1.2.xの場合
###ポイント
AngularJS 1.2.x系では、Directive間にて値のやり取りをするには`$scope`を用いるのが一番簡単のように思えます。しかしながら`controllerAs`を用いた方がDirectiveがネストされた時の煩雑さを防げます([参考](http://qiita.com/Quramy/items/e55ba394031794236fc9))。よって`controllerAs`を使いたいのですが、コントローラにおいて変更を感知するには`$scope.$watch`を用いるしかないため、書き方が複雑になってしまいます。
###ソース

```html
<div ng-app="myApp">
    <parent-directive>
    </parent-directive>        
</div>
```

```parent.js
angular.module("myApp",[]);

//---親のDirective
//------親のDirectiveのcontroller
function parentController(){
    this.num = 0;
    this.increment = function(){
        this.num++;
    }
}
//------親のDirectiveのDirective Definition Object
function parentDirective(){
    var ddo = {
        restrict: "E",
        scope: {},
        controller: parentController,
        controllerAs: "parent",
        template: [
            "<div style='background-color: #f0f0f0'>",
                "<span>num: {{parent.num}} in parent </span>",
                "<button ng-click='parent.increment()'>+1</button>",
                "<child-directive prop1='parent.num'></child-directive>",
            "</div>"].join("")
    };
    return ddo;
}
angular.module("myApp").directive("parentDirective", parentDirective);
```

```child.js
//---子のDirective
//------子のDirectiveのcontroller
function childController($scope){

    //ここでのthisは、templateの{{child}}。(controllerAs)
    //numのをwatchしなければ変更をthisに伝えることはできない。
    
    $scope.$watch("num", function(newVal){
        this.num = newVal;
    }.bind(this));
}
//------子のDirectiveのDirective Definition Object
function childDirective(){
    var ddo = {
        restrict: "E",
        scope: {
        	num: "=prop1"
        },
        controller: childController,
        controllerAs: "child",
        template: [
            "<div style='background-color: #d0d0d0'>",
                "<span>num: {{child.num}} in child </span>",
            "</div>"].join("")
    };
    return ddo;
}
angular.module("myApp").directive("childDirective", childDirective);

```

https://jsfiddle.net/f73x4Law/3/

##AngularJS 1.3.xの場合
###ポイント
AngularJS 1.3.xでは`bindToController`を利用できます。`bindToController: true`とすることにより`controllerAs`を用いてコントローラ内で`this`を通じて値を参照できます。`$scope.$watch`を用いずとも、親の変更を反映することできます。childControllerを見れば楽に書けるようになったことは明らかです。
###ソース

```parent.js
angular.module("myApp",[]);

//---親のDirective
//------親のDirectiveのcontroller
function parentController(){
    this.num = 0;
    this.increment = function(){
        this.num++;
    }
}
//------親のDirectiveのDirective Definition Object
function parentDirective(){
    var ddo = {
        restrict: "E",
        scope: {},
        controller: parentController,
        controllerAs: "parent",
        template: [
            "<div style='background-color: #f0f0f0'>",
                "<span>num: {{parent.num}} in parent </span>",
                "<button ng-click='parent.increment()'>+1</button>",
                "<child-directive prop1='parent.num'></child-directive>",
            "</div>"].join("")
    };
    return ddo;
}
angular.module("myApp").directive("parentDirective", parentDirective);
```

```child.js
//---子のDirective
//------子のDirectiveのcontroller
function childController(){

    //ここでのthisは、templateの{{child}}。(controllerAs)
    //bindToControllerによって、scopeがthis.~に紐づく。
    
    console.log(this.num);
}
//------子のDirectiveのDirective Definition Object
function childDirective(){
    var ddo = {
        restrict: "E",
        scope: {
        	num: "=prop1"
        },
        controller: childController,
        controllerAs: "child",
        
        //bindToCotrollerをtrueにすることでthisとscopeを紐づけることができる。
        
        bindToController: true,
        template: [
            "<div style='background-color: #d0d0d0'>",
                "<span>num: {{child.num}} in child </span>",
            "</div>"].join("")
    };
    return ddo;
}
angular.module("myApp").directive("childDirective", childDirective);

```

https://jsfiddle.net/f73x4Law/4/

##AngularJS 1.4.xの場合
###ポイント
AngularJS 1.4.xでは、`bindToController`にて紐づける値を直接指定することができます。`bindToController:{hoge: "="}`と書いていきます。スコープオプションは書かなくてもいい。しかしながら、`scope:{}`もしくは`scope:true`と書いておかないと動きません。ので、分離スコープを生成する`scope:{}`を指定すればよいでしょう。
###ソース

```parent.js
angular.module("myApp",[]);

//---親のDirective
//------親のDirectiveのcontroller
function parentController(){
    this.num = 0;
    this.increment = function(){
        this.num++;
    }
    
    //this.notBindはbindToControllerにてbindしない場合。
    
    this.notBind = "notBind";
}
//------親のDirectiveのDirective Definition Object
function parentDirective(){
    var ddo = {
        restrict: "E",
        scope: {},
        controller: parentController,
        controllerAs: "parent",
        template: [
            "<div style='background-color: #f0f0f0'>",
                "<span>num: {{parent.num}} in parent </span>",
                "<button ng-click='parent.increment()'>+1</button>",
                "<child-directive prop1='parent.num' prop2='parent.notBind'></child-directive>",
            "</div>"
        ].join("")
    };
    return ddo;
}
angular.module("myApp").directive("parentDirective", parentDirective);
```


```child.js
//---子のDirective
//------子のDirectiveのcontroller
function childController(){
    //ここでのthisは、templateの{{child}}。(controllerAs)
    //bindToControllerによって、scopeがthis.~に紐づく。

    console.log(this.num);

    //this.notBindは紐づきません。
    
    console.log(this.notBind);
}
//------子のDirectiveのDirective Definition Object
function childDirective(){
    var ddo = {
        restrict: "E",
        scope: {
            notBind: "=prop2"
        },
        controller: childController,
        controllerAs: "child",
        //1.4からはbindToCotrollerにて直接bindする値を選択できる。
        bindToController: {
            num: "=prop1"
        },
        template: [
            "<div style='background-color: #d0d0d0'>",
                "<span>num: {{child.num}} in child </span>",
            "</div>"
        ].join("")
    };
    return ddo;
}
angular.module("myApp").directive("childDirective", childDirective);
```

https://jsfiddle.net/f73x4Law/2/

#注意点
自作Directiveを作ろうとした時に筆者がよくつまずく点を記しておきます。
##2語以上のDirective名だと動かない
`angular.module().directive("sampleDirective", sampleDirective);`
とした時のHTML要素は
`<sampleDirective></sampleDirective>`
ではなく
`<sample-directive></sample-directive>`
##2語以上のscope名だと動かない
`{child: "=sampleModel"}`
のとき
`<sample-directive sampleModel='data'></sample-directive>`
ではなく
`<sample-directive sample-model='data'></sample-directive>`
Directive名はキャメルケース、テンプレート上での名前はハイフンケース

##bindToControllerがうまく動かない
`bindToController: {hoge: "=foo"}`
としたけど動かない。これはかなりひっかけではないか。
上でも書いたが、`scope:{}`もしくは`scope:true`と書いておかないと動きません。

#最後に
1.4ではDirectiveの扱いが簡単に、そして分かりやすく、便利になったと思います。
今回の話題を、[Angular Meetup Kyoto #1](http://ng-kyoto.connpass.com/event/15595/) @京都6.19 presented by [ng-kyoto](http://ng-kyoto.github.io/)にてもう少し詳しく話します。お時間がありましたらお越しください。