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