連載企画「AngularJS徹底解説」の第3回目は、ControllerとScopeの基礎について解説していきます。
AngularJS は MVW(hatever)!!!
AngularJSはMVC(Model-View-Controller)フレームワークと呼ばれることが多いですが、一部の開発者からは MVVM(Model-View-ViewModel)である、という声もあがっていて、ある時期に「一体どっちなんだ!?」という状態になりました。
そこで、そういった経緯に対してAngularJSチームが、そこについての議論は本筋ではないとして、AngularJSはMVW(Model-View-Whatever)である、と明言しています。
今回解説するControllerは、このWhateverの役割にあたる機能です。実際に「Whatever = 何でもいい」とはいっても、”Controller”という名のとおり、MVCパターンとして見れば M-V-“Controller”にあたる機能ですので、その概念から外れることはありません。
Controller の基本
コントローラは以下のように記述します。他にもいくつか書き方があるのですが、今回はこの書き方で解説していきます。
まず、myApp
アプリにmyCtrl
というコントローラを作成してみます。
1 2 3 4 |
angular.module('myApp', []) .controller('myCtrl', ['$scope', function($scope){ // ここにコードを書いていく }]); |
以下の意味になります。
1 2 3 4 |
... .controller.('コントローラ名', ['依存サービス', function(依存サービス){ // ここにコードを書いていく }]); |
$scopeについては、 ビューにデータなどを渡したり、ビューから発生したイベントを監視するなど、ビューとのやり取りを行うことができる特別なオブジェクトです。(AngularJSでは直接DOM操作をすることは推奨されていないため、この$scopeを通じて行います。そのためほとんどのケースでコントローラを記述する際に定義することになります)
また複数依存するサービスが複数ある場合は、
1 2 3 4 |
... .controller.('コントローラ名', ['依存サービス1', '依存サービス2' ... , function(依存サービス1, 依存サービス2 ...){ // ここにコードを書いていく }]); |
このように、配列に追加していく形で定義することができます。
先ほどの$scopeを利用して、ビューにモデルの値を引き渡します。
テンプレート(HTML)箇所については、前回までに解説した通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<!DOCTYPE html> <html lang="ja" ng-app="myApp"> <head> <meta charset="UTF-8"> <title>Controller</title> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js"></script> </head> <body ng-controller="myCtrl"> <div>筆者:{{myName}}</div> <div> エキスパートたち <ul> <li ng-repeat="expert in expertList">{{expert}}</li> </ul> </div> <div> {{author.name}} ( twitter - {{author.twitter}} ) </div> <script> angular.module('myApp', []) .controller('myCtrl', ['$scope', function($scope){ // Model の初期化 ① var myName = 'can.i.do.web'; var expertList = ['Shumpei', 'komasshu', 'yoshikawa_t']; var author = { name: 'きゃない', twitter: 'can_i_do_web' }; // View にバインディング ② $scope.myName = myName; $scope.expertList = expertList; $scope.author = author; }]); </script> </body> </html> |
$socpeを利用することで、ビューとやりとりすることができます。①で様々な形式のモデルの初期化、②で$scopeにプロパティを追加していくかたちで、 ビューに値を渡すことができます。
上記のほかにも複雑なオブジェクトや、関数などもバインドすることが可能です。
Controller の適用範囲
コントローラに限らず、ではあるのですが、AngularJSのディレクティブには適用範囲があります。
それは、そのディレクティブを定義したDOMの範囲とイコールです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
... <body> <div ng-controller="myCtrl"> myCtrl内: {{name}} <!-- ① --> </div> myCtrl外: {{name}} <!-- ② --> <script> angular.module('myApp', []) .controller('myCtrl', ['$scope', function($scope){ $scope.name = 'can.i.do.web'; }]); </script> </body> |
①のようにng-controller="myCtrl"
と定義したdivタグの内部のみ、初期化されたname
を表示(取り扱う)することができます。
②はng-controller="myCtrl"
が定義されているdivタグの外側にあるため、myCtrl
で定義したname
にアクセスすることはできません。
このように、AngularJSではそれぞれの コントローラなどのディレクティブがお互いに干渉しないよう、適用される範囲のみでそれぞれの機能を果たします。そうすることで個々の機能が疎結合になり、保守性が高くなります。
Controller の分割
最初に挙げたサンプルのように画面全体に対して1つのControllerで運用していった場合、かなり多機能なコントローラになってしまいます。
例えば以下のケースだとどうなっているか見ていきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
... <body ng-controller="myCtrl"> <header> ... <!-- header --> </header> <main> ... <!-- main --> </main> <footer> ... <!-- footer --> </footer> <script> angular.module('myApp', []) .controller('myCtrl', ['$scope', function($scope){ // いろいろな機能のコード }]); </script> |
上記はbody
全体に対してのみのコントローラとなっています。ヘッダやフッタに関しては、一般的なアプリケーションでは共通的な機能と言えるでしょう。
それに対して上記の場合は、画面ごとにヘッダやフッタのデータやイベント等の振る舞いを毎回記述しなくてはなりません。先述したとおり、保守性を高めるために、 コントローラを分割すると良いでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... <body ng-controller="myCtrl"> <header ng-controller="headerCtrl"> ... <!-- header --> </header> <main ng-controller="mainCtrl"> ... <!-- main --> </main> <footer ng-controller="footerCtrl"> ... <!-- footer --> </footer> <script> angular.module('myApp', []) .controller('myCtrl', ['$scope', function($scope){ // 画面全体に渡るコード }]) .controller('headerCtrl', ['$scope', function($scope){ // headerで必要な機能のコード }]) .controller('mainCtrl', ['$scope', function($scope){ // main で必要な機能のコード }]) .controller('footerCtrl', ['$scope', function($scope){ // footer で必要な機能のコード }]); </script> |
上記のようにコントローラを分割することで、機能の塊を小さくできます。headerCtrl
やfooterCtrl
を切り分けることによって、アプリケーション全体で共通的なヘッダ、フッタの機能として利用することもできます。
また、メインコンテンツ内も機能的に煩雑になっているようであれば、さらにコントローラを分割して運用していくとよいでしょう。
Scope
スコープはビューとコントローラの間に立って、モデルをビューにバインディングしたり、ビューから発生したイベントを受け取って何かしら振る舞う、といった役割を持ちます。$scopeはそれを実際に行なうためのオブジェクトです。
スコープはDOMツリーと同様ツリー構造になっていて、ng-app
を基点にDOMツリーに沿うように構成されます。そしてng-controller
と記述している箇所でng-app
の子スコープとして新たなスコープが生成されています。Chrome DevToolsなどで該当するDOMを見てみると、ng-scope
というクラス名が自動的に付与されているため、これを元に確認することができます。
最初のサンプルの場合、ng-repeat
でも新しくスコープを生成するため、expertList
の<li>
にも同じように付与されています。
“Controllerの適用範囲” で解説した内容はこのスコープのツリー構造と関わっています。
以下の例を見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
... <body ng-controller="myCtrl"> <p>①親スコープの'name' -> {{name}}</p> <p>②親から見ようとした 'twitter' -> {{twitter}}</p> <div ng-controller="myChildCtrl"> <p>③myChildCtrl の 'name' -> {{name}}</p> <p>④myChildCtrl の 'twitter' -> {{twitter}}</p> </div> <script> angular.module('myApp', []) .controller('myCtrl', ['$scope', function($scope){ $scope.name = 'can.i.do.web'; }]) .controller('myChildCtrl', ['$scope', function($scope){ $scope.twitter = 'きゃない'; }]); </script> |
実行してみると、このように表示されます。
スコープはツリー上に構成されるため、スコープの親子関係が生まれます。
原則、子スコープから親スコープの値は参照できますが、その反対はできません。
親であるmyCtrl
で定義された、name
というプロパティは、子であるmyChildCtrl
のname
として参照することができるため、表示されています。
反対に、親スコープから子スコープのプロパティを参照できないため、②では何も表示されていません。
子スコープが新たに生成されるタイミングは、コントローラを定義する(ng-controller)など、一部の(ビルトイン)ディレクティブ などを定義した場合です。
どのディレクティブ が新しくスコープを生成するかや、詳細な挙動については、AngularJS にかなり慣れている必要があります。
しかし、はじめから完璧に理解する必要は全くありません。スコープという概念があり有効範囲や親子関係がある、ということを頭の片隅においておくと良いでしょう。
Controller 間でデータ共有
親子コントローラ間でデータを共有するためには以下のように記述しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... <body ng-controller="parentCtrl"> <div> <p>親スコープ</p> <input type="text" ng-model="autor.name"> </div> <div ng-controller="childCtrl"> <p>子スコープ</p> <input type="text" ng-model="autor.name"> </div> <script> angular.module('myApp', []) .controller('parentCtrl', ['$scope', function($scope){ // 親スコープ var author = {}; author.name = 'Kanai'; // bind $scope.autor = author; }]) .controller('childCtrl', ['$scope', function($scope){ // 子スコープ }]); </script> |
これまでのサンプルとの違いは、$scope.autor.name
のように、直接$scope.name
としていない点です。
このように$scope
とバインドしたいプロパティとの間に、オブジェクトを挟むことで、親子間でデータを共有することができます。
こうすることで、親子スコープのどちらのinput
でも、片方の入力値を書き換えれば、もう一方のデータも書き変わります。
先述してきたサンプルの場合は、JavaScriptのプロトタイプチェーンの都合上、親子関係が破綻してしまいます。そのため、値を書き換えても相互にデータの書き換えを行なうことができません。
データの書き換えや参照が思ったようにうまくいかない場合は、このことを覚えておくとよいかもしれません。
まとめ
今回は、ControllerとScopeの基礎を解説しました。
特にスコープについては複雑な概念や実装になっていますので、いきなり全てを把握することは難しいです。AngularJS の経験を積むとともに徐々に学んでいきましょう。
次回はサービスについての解説を予定しています。