その使い方はもう古いかも？AngularJS老化チェック（ディレクティブ篇）


こんにちは、らこです。AngularJSのバージョン1.5.0がリリースされましたね！

http://angularjs.blogspot.jp/2016/02/angular-150-ennoblement-facilitation.html

コードネームは **ennoblement-facilitation**、ざっくり訳すなら「高尚化促進」でしょうか。コンポーネント志向の「高尚」なアプリケーション設計への移行をサポートするバージョンだということでしょう。

1.5.0では`component`の追加をメインに、Angular2へのスムーズな移行を行うための足がかりとなるアップデートです。いい機会なので、 **今の自分のAngularJSの使い方がどれくらい古いのか** をチェックし、どのようにモダンにしていけばいいのかを知っておきましょう。

ちなみに、1.4までに関しては [AngularJSモダンプラクティス - Qiita](http://qiita.com/armorik83/items/5542daed0c408cb9f605) を参考にするとよいでしょう。
このモダンプラクティスに従ったコードになっていれば`component()`対応はあっという間のはずです。

### 到達目標 - component()
1.5.0から可能になる`component()`によるカスタム要素の定義は次のように記述します。

```html
  <body>
    <my-app></my-app>
    <script src="app.js"></script>
  </body>
```

```js
const app = angular.module("app", []);

class MyAppCtrl {
}

app.component("myApp", {
  template: `<greeting name="'World'"></greeting>`,
  controller: MyAppCtrl
});

class GreetingCtrl {
  
  get upperName() {
    return this.name.toUpperCase();
  }
}

app.component("greeting", {
  bindings: {
    name: "="
  },
  template: `<h1>Hello {{$ctrl.upperName}}!</h1>`,
  controller: GreetingCtrl
});

angular.bootstrap(document.body, [app.name]);
```

[サンプル](http://plnkr.co/edit/ozql0SUBcPetR5xJQ6Kc?p=preview)

ポイントは次の3点です

* controllerにES6 Classを渡している
* `$scope`を使っていない
* componentのテンプレート内で`$ctrl`を使っている 

ES6での記述は機能とは直接関係はありませんが、Angular2を見据えたコードにするならばクラスにしておいて損はありません。フィールドの初期化もコンストラクタがあるので明確です。

データバインディングに`$scope`を使っていないことも重要です。`component`を使うと、デフォルトで`controllerAs: "$ctrl"`の状態になります。コンポーネント内の閉じたスコープが自動的に与えられ、コントローラのフィールドや関数に直接バインドすることができます。

なお、`component()`になっても`directive()`と同じく定義するカスタム要素の名前はキャメルケースで **myApp** のようにします。そしてHTML側ではハイフンケースで **my-app** のように宣言します。


### モダンなAngularJS - directive + scope + bTC + controllerAs
1.4までは`component()`がないので、`directive()`でしかカスタム要素は定義できません。しかし`scope`プロパティと`bindToController` (bTC)、`controllerAs`を使うことで同じようなコンポーネントを定義することができます。

```js
function MyAppCtrl() {
}

app.directive("myApp", () => {
  return {
    restrict: "E",
    scope: {},
    template: `<greeting name="'World'"></greeting>`,
    controller: MyAppCtrl
  };
});

function GreetingCtrl() {
  this.upperName = () => this.name.toUpperCase();
}

app.directive("greeting", () => {
  return {
    restrict: "E",
    scope: {},
    bindToController: {
      name: "="
    },
    template: `<h1>Hello {{$ctrl.upperName()}}!</h1>`,
    controller: GreetingCtrl,
    controllerAs: "$ctrl"
  };
});
```

[サンプル](http://plnkr.co/edit/AZvbhcGJl1FDpAWbScGF?p=preview)

実は1.5の`component()`はこれと同じような`directive()`の呼び出しをラップしているだけです。また、bTCにオブジェクトを渡せるようになったのは1.4からです。

### ナウいAngularJS - directive + scope + controllerAs
1.3まではbTCにオブジェクトが渡せないので`scope`と併用しないといけません。

```js
function MyAppCtrl() {
}

app.directive("myApp", function() {
  return {
    restrict: "E",
    scope: {},
    template: "<greeting name=\"'World'\"></greeting>",
    controller: MyAppCtrl
  };
});

function GreetingCtrl() {
  this.upperName = function() {
    return this.name.toUpperCase();
  }
}

app.directive("greeting", function() {
  return {
    restrict: "E",
    scope: {
      name: "="
    },
    bindToController: true,
    template: "<h1>Hello {{$ctrl.upperName()}}!</h1>",
    controller: GreetingCtrl,
    controllerAs: "$ctrl"
  };
});
```

[サンプル](http://plnkr.co/edit/JTEQClW1Sjc5Xgi3Zh4C?p=preview)

1.4のコードと殆ど変わらないですね。ここまでできていればほとんどコンポーネント化できていると言えるでしょう

### 古い - directive + scope + controllerAs
1.2以前はbTCがないので、親スコープからのデータバインディングは`$scope`を介さないと受け取ることができません。しかし、`controllerAs`を使っているのでまだ自身のプロパティに関してはthisで扱えます。

```js
function MyAppCtrl() {
}

app.directive("myApp", function () {
  return {
    restrict: "E",
    scope: {},
    template: "<greeting name=\"'World'\"></greeting>",
    controller: MyAppCtrl
  };
});

function GreetingCtrl($scope) {
  this.upperName = function() {
     return $scope.name.toUpperCase();
  };
}

app.directive("greeting", function() {
  return {
    restrict: "E",
    scope: {
      name: "="
    },
    template: "<h1>Hello {{$ctrl.upperName()}}!</h1>",
    controller: GreetingCtrl,
    controllerAs: "$ctrl"
  };
});
```

[サンプル](http://plnkr.co/edit/RNCJytuPAbP5De33rKNv?p=preview)

ここまでは比較的すんなり`component()`に移行しやすい（スコープの考え方がだいたい合ってる）コードだと思います。

### 時代遅れ - directive + scope + controller
`controllerAs`が導入されていないのでテンプレート中で変数名が直接登場し、コントローラのthisがスコープで参照されなくなりました。ただし`scope`を使っていてisolated scopeがあります。また、コントローラでビューとロジックの分離がギリギリ出来ている状態です。

```js
function MyAppCtrl() {
}

app.directive("myApp", function() {
  return {
    restrict: "E",
    scope: {},
    template: "<greeting name=\"'World'\"></greeting>",
    controller: MyAppCtrl
  };
});

function GreetingCtrl($scope) {
  $scope.upperName = function() {
    return $scope.name.toUpperCase();
  };
}

app.directive("greeting", function() {
  return {
    restrict: "E",
    scope: {
      name: "="
    },
    template: "<h1>Hello {{upperName()}}!</h1>",
    controller: GreetingCtrl,
  };
});
```

[サンプル](http://plnkr.co/edit/bZYSsue2KAHoNAUFq0eF?p=preview)

このコードは1.5関係なく、早めに`controllerAs`を使ったスタイルに直すべきです。`controllerAs`についての議論は2014年あたりには決着がついており、

- [AngularJS: "Controller as" or "$scope"?](http://codetunnel.com/angularjs-controller-as-or-scope/?utm_content=bufferbbbf1&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer)
- [Digging into Angular’s “Controller as” syntax @toddmotto](https://toddmotto.com/digging-into-angulars-controller-as-syntax)

この辺りを読めば`controllerAs`を使うことによるメリット、使わないことによるデメリットがわかるはずです。

### 化石 - directive + link
directiveに紐付いたスコープが存在しないケースです。`link`関数や`compile`関数は完全に`component()`と互換性があるわけではないですが、カスタム要素の場合はほとんどのケースで`link`も`compile`も必要ないでしょう。directiveとcontrollerでロジックの分離もできていないのでとてもメンテ性の低いコードです。

```js
app.directive("myApp", function() {
  return {
    restrict: "E",
    template: "<greeting name=\"World\"></greeting>"
  };
});

app.directive("greeting", function() {
  return {
    restrict: "E",
    template: "<h1>Hello {{upperName()}}!</h1>",
    link: function(scope, element, attrs) {
      scope.name = attrs.name;
      scope.upperName = function() {
        return scope.name.toUpperCase();
      };
    }
  };
});
```

[サンプル](http://plnkr.co/edit/3Mbxd5wSBAfIt9RQvHE5?p=preview)


### 論外 - html + ng-controller
カスタム要素によるコンポーネント化が全くなされていないコードです。

```html
    <div id="myApp" ng-controller="MyAppCtrl">
      <div id="greeting" ng-controller="GreetingCtrl">
        <h1>Hello {{upperName()}}!</h1>
      </div>
    </div>
```

```js
app.controller("MyAppCtrl", ["$scope", function($scope) {
    $scope.name = "World";
}]);

app.controller("GreetingCtrl", ["$scope", function($scope) {
  $scope.upperName = function() {
    return $scope.name.toUpperCase();
  };
}]);
```

[サンプル](http://plnkr.co/edit/h1h3YuEHi1t2BkNp2IZX?p=preview)

綺麗にAngularJSを使うつもりがあれば`ng-controller`は無くしましょう。せめて`as`を使って親スコープからの暗黙の継承を防ぎましょう。

## これからやるべきこと
自分のAngularJSの現在地がなんとなくわかったでしょうか。それぞれの段階ごとに、適切な順序でコンポーネント志向なAngularJSにしていきましょう。

### html + ng-controllerが残ってる場合
1. まずは`ng-controller="SomeCtrl as some"`にする
2. `ng-controller`のある要素をdirectiveに置き換える

すべてのコントローラを`controllerAs`にするのが最初の最優先課題です。
それができればあとは一番外側のコントローラから順番にdirectiveにしていきましょう。

### directiveがcontrollerを使っていない場合
1. directiveの`link`と`compile`はコントローラで代用
2. directiveの`scope`でスコープを分離する

次に最優先すべきは`scope`を使ってdirectiveごとにスコープを分離することです。

### bTCを使っていない場合
1. AngularJS1.4以降に上げる
2.`scope`に渡していたオブジェクトをbTCに渡し、`scope`を空オブジェクトにする

次は`$scope`を使わずにすむようにdirectiveのスコープをコントローラにバインドしましょう。

### 1.4でbTCを使っていた場合
1. AngularJS 1.5に上げる
2. `directive()`の代わりに`component()`を使う
3. できればコントローラはES6 Classにする

完成です。ここまでくればAngular2も今いる場所の延長線上にあることがわかってくると思います。

## まとめ
1.2から1.3、1.4とコンポーネント志向なdirectiveを作るための機能が少しずつ増えてきており、それらのベストプラクティスとして生まれたのが`component()`です。
1.5以降はもうAngularJSとしての新しい機能は増えず、Angular2への移行サポートがメインになることがわかっています。
`component()`対応後にちゃぶ台を返される心配はないので、安心して1.5に向けたマイグレーションを行いましょう。