こんにちは、らこです。AngularJSのバージョン1.5.0がリリースされましたね!
コードネームは 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()と互換性があるわけではないですが、カスタム要素の場合はほとんどのケースでlinkもcompileも必要ないでしょう。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が残ってる場合
- まずは
ng-controller="SomeCtrl as some"にする -
ng-controllerのある要素をdirectiveに置き換える
すべてのコントローラをcontrollerAsにするのが最初の最優先課題です。
それができればあとは一番外側のコントローラから順番にdirectiveにしていきましょう。
directiveがcontrollerを使っていない場合
- directiveの
linkとcompileはコントローラで代用 - directiveの
scopeでスコープを分離する
次に最優先すべきはscopeを使ってdirectiveごとにスコープを分離することです。
bTCを使っていない場合
- AngularJS1.4以降に上げる
2.scopeに渡していたオブジェクトをbTCに渡し、scopeを空オブジェクトにする
次は$scopeを使わずにすむようにdirectiveのスコープをコントローラにバインドしましょう。
1.4でbTCを使っていた場合
- AngularJS 1.5に上げる
-
directive()の代わりにcomponent()を使う - できればコントローラはES6 Classにする
完成です。ここまでくればAngular2も今いる場所の延長線上にあることがわかってくると思います。
まとめ
1.2から1.3、1.4とコンポーネント志向なdirectiveを作るための機能が少しずつ増えてきており、それらのベストプラクティスとして生まれたのがcomponent()です。
1.5以降はもうAngularJSとしての新しい機能は増えず、Angular2への移行サポートがメインになることがわかっています。
component()対応後にちゃぶ台を返される心配はないので、安心して1.5に向けたマイグレーションを行いましょう。