その使い方はもう古いかも?AngularJS老化チェック(ディレクティブ篇)

  • 401
    いいね
  • 5
    コメント
この記事は最終更新日から1年以上が経過しています。

こんにちは、らこです。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 を参考にするとよいでしょう。
このモダンプラクティスに従ったコードになっていればcomponent()対応はあっという間のはずです。

到達目標 - component()

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

  <body>
    <my-app></my-app>
    <script src="app.js"></script>
  </body>
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]);

サンプル

ポイントは次の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を使うことで同じようなコンポーネントを定義することができます。

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"
  };
});

サンプル

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

ナウいAngularJS - directive + scope + controllerAs

1.3まではbTCにオブジェクトが渡せないのでscopeと併用しないといけません。

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"
  };
});

サンプル

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

古い - directive + scope + controllerAs

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

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"
  };
});

サンプル

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

時代遅れ - directive + scope + controller

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

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,
  };
});

サンプル

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

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

化石 - directive + link

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

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();
      };
    }
  };
});

サンプル

論外 - html + ng-controller

カスタム要素によるコンポーネント化が全くなされていないコードです。

    <div id="myApp" ng-controller="MyAppCtrl">
      <div id="greeting" ng-controller="GreetingCtrl">
        <h1>Hello {{upperName()}}!</h1>
      </div>
    </div>
app.controller("MyAppCtrl", ["$scope", function($scope) {
    $scope.name = "World";
}]);

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

サンプル

綺麗に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のlinkcompileはコントローラで代用
  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に向けたマイグレーションを行いましょう。