HTML5Experts.jp

AngularJSのControllerとScopeの基礎を学ぼう

連載企画「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というコントローラを作成してみます。

angular.module('myApp', [])
  .controller('myCtrl', ['$scope', function($scope){
    // ここにコードを書いていく
  }]);
以下の意味になります。
...
  .controller.('コントローラ名', ['依存サービス', function(依存サービス){
    // ここにコードを書いていく
  }]);

$scopeについては、 ビューにデータなどを渡したり、ビューから発生したイベントを監視するなど、ビューとのやり取りを行うことができる特別なオブジェクトです。(AngularJSでは直接DOM操作をすることは推奨されていないため、この$scopeを通じて行います。そのためほとんどのケースでコントローラを記述する際に定義することになります)

また複数依存するサービスが複数ある場合は、

...
  .controller.('コントローラ名', ['依存サービス1', '依存サービス2' ... , function(依存サービス1, 依存サービス2 ...){
    // ここにコードを書いていく
  }]);

このように、配列に追加していく形で定義することができます。

先ほどの$scopeを利用して、ビューにモデルの値を引き渡します。
テンプレート(HTML)箇所については、前回までに解説した通りです。

<!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の範囲とイコールです。

...
<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で運用していった場合、かなり多機能なコントローラになってしまいます。

例えば以下のケースだとどうなっているか見ていきましょう。

...
<body ng-controller="myCtrl">
  <header>
    ... <!-- header -->
  </header>
  <main>
    ... <!-- main -->
  </main>
  <footer>
    ... <!-- footer -->
  </footer>
<script>
angular.module('myApp', [])
  .controller('myCtrl', ['$scope', function($scope){
    // いろいろな機能のコード
  }]);
</script>

上記はbody全体に対してのみのコントローラとなっています。ヘッダやフッタに関しては、一般的なアプリケーションでは共通的な機能と言えるでしょう。

それに対して上記の場合は、画面ごとにヘッダやフッタのデータやイベント等の振る舞いを毎回記述しなくてはなりません。先述したとおり、保守性を高めるために、 コントローラを分割すると良いでしょう。

...
<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>

上記のようにコントローラを分割することで、機能の塊を小さくできます。headerCtrlfooterCtrlを切り分けることによって、アプリケーション全体で共通的なヘッダ、フッタの機能として利用することもできます。

また、メインコンテンツ内も機能的に煩雑になっているようであれば、さらにコントローラを分割して運用していくとよいでしょう。

Scope

スコープはビューとコントローラの間に立って、モデルをビューにバインディングしたり、ビューから発生したイベントを受け取って何かしら振る舞う、といった役割を持ちます。$scopeはそれを実際に行なうためのオブジェクトです。

スコープはDOMツリーと同様ツリー構造になっていて、ng-appを基点にDOMツリーに沿うように構成されます。そしてng-controller と記述している箇所でng-appの子スコープとして新たなスコープが生成されています。Chrome DevToolsなどで該当するDOMを見てみると、ng-scopeというクラス名が自動的に付与されているため、これを元に確認することができます。

最初のサンプルの場合、ng-repeatでも新しくスコープを生成するため、expertList<li>にも同じように付与されています。

“Controllerの適用範囲” で解説した内容はこのスコープのツリー構造と関わっています。

以下の例を見てみましょう。

...
<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というプロパティは、子であるmyChildCtrlnameとして参照することができるため、表示されています。
反対に、親スコープから子スコープのプロパティを参照できないため、②では何も表示されていません。

子スコープが新たに生成されるタイミングは、コントローラを定義する(ng-controller)など、一部の(ビルトイン)ディレクティブ などを定義した場合です。
どのディレクティブ が新しくスコープを生成するかや、詳細な挙動については、AngularJS にかなり慣れている必要があります。
しかし、はじめから完璧に理解する必要は全くありません。スコープという概念があり有効範囲や親子関係がある、ということを頭の片隅においておくと良いでしょう。

Controller 間でデータ共有

親子コントローラ間でデータを共有するためには以下のように記述しましょう。

...
<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 の経験を積むとともに徐々に学んでいきましょう。
次回はサービスについての解説を予定しています。