AngularJS

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

More than 3 years have passed since last update.

こんにちは、らこです。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に向けたマイグレーションを行いましょう。