連載企画「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
というコントローラを作成してみます。
以下の意味になります。
$scopeについては、 ビューにデータなどを渡したり、ビューから発生したイベントを監視するなど、ビューとのやり取りを行うことができる特別なオブジェクトです。(AngularJSでは直接DOM操作をすることは推奨されていないため、この$scopeを通じて行います。そのためほとんどのケースでコントローラを記述する際に定義することになります)
また複数依存するサービスが複数ある場合は、
このように、配列に追加していく形で定義することができます。
先ほどの$scopeを利用して、ビューにモデルの値を引き渡します。
テンプレート(HTML)箇所については、前回までに解説した通りです。
$socpeを利用することで、ビューとやりとりすることができます。①で様々な形式のモデルの初期化、②で$scopeにプロパティを追加していくかたちで、 ビューに値を渡すことができます。
上記のほかにも複雑なオブジェクトや、関数などもバインドすることが可能です。
Controller の適用範囲
コントローラに限らず、ではあるのですが、AngularJSのディレクティブには適用範囲があります。
それは、そのディレクティブを定義したDOMの範囲とイコールです。
①のようにng-controller="myCtrl"
と定義したdivタグの内部のみ、初期化されたname
を表示(取り扱う)することができます。
②はng-controller="myCtrl"
が定義されているdivタグの外側にあるため、myCtrl
で定義したname
にアクセスすることはできません。
このように、AngularJSではそれぞれの コントローラなどのディレクティブがお互いに干渉しないよう、適用される範囲のみでそれぞれの機能を果たします。そうすることで個々の機能が疎結合になり、保守性が高くなります。
Controller の分割
最初に挙げたサンプルのように画面全体に対して1つのControllerで運用していった場合、かなり多機能なコントローラになってしまいます。
例えば以下のケースだとどうなっているか見ていきましょう。
上記はbody
全体に対してのみのコントローラとなっています。ヘッダやフッタに関しては、一般的なアプリケーションでは共通的な機能と言えるでしょう。
それに対して上記の場合は、画面ごとにヘッダやフッタのデータやイベント等の振る舞いを毎回記述しなくてはなりません。先述したとおり、保守性を高めるために、 コントローラを分割すると良いでしょう。
上記のようにコントローラを分割することで、機能の塊を小さくできます。headerCtrl
やfooterCtrl
を切り分けることによって、アプリケーション全体で共通的なヘッダ、フッタの機能として利用することもできます。
また、メインコンテンツ内も機能的に煩雑になっているようであれば、さらにコントローラを分割して運用していくとよいでしょう。
Scope
スコープはビューとコントローラの間に立って、モデルをビューにバインディングしたり、ビューから発生したイベントを受け取って何かしら振る舞う、といった役割を持ちます。$scopeはそれを実際に行なうためのオブジェクトです。
スコープはDOMツリーと同様ツリー構造になっていて、ng-app
を基点にDOMツリーに沿うように構成されます。そしてng-controller
と記述している箇所でng-app
の子スコープとして新たなスコープが生成されています。Chrome DevToolsなどで該当するDOMを見てみると、ng-scope
というクラス名が自動的に付与されているため、これを元に確認することができます。
最初のサンプルの場合、ng-repeat
でも新しくスコープを生成するため、expertList
の<li>
にも同じように付与されています。
“Controllerの適用範囲” で解説した内容はこのスコープのツリー構造と関わっています。
以下の例を見てみましょう。
実行してみると、このように表示されます。
スコープはツリー上に構成されるため、スコープの親子関係が生まれます。
原則、子スコープから親スコープの値は参照できますが、その反対はできません。
親であるmyCtrl
で定義された、name
というプロパティは、子であるmyChildCtrl
のname
として参照することができるため、表示されています。
反対に、親スコープから子スコープのプロパティを参照できないため、②では何も表示されていません。
子スコープが新たに生成されるタイミングは、コントローラを定義する(ng-controller)など、一部の(ビルトイン)ディレクティブ などを定義した場合です。
どのディレクティブ が新しくスコープを生成するかや、詳細な挙動については、AngularJS にかなり慣れている必要があります。
しかし、はじめから完璧に理解する必要は全くありません。スコープという概念があり有効範囲や親子関係がある、ということを頭の片隅においておくと良いでしょう。
Controller 間でデータ共有
親子コントローラ間でデータを共有するためには以下のように記述しましょう。
これまでのサンプルとの違いは、$scope.autor.name
のように、直接$scope.name
としていない点です。
このように$scope
とバインドしたいプロパティとの間に、オブジェクトを挟むことで、親子間でデータを共有することができます。
こうすることで、親子スコープのどちらのinput
でも、片方の入力値を書き換えれば、もう一方のデータも書き変わります。
先述してきたサンプルの場合は、JavaScriptのプロトタイプチェーンの都合上、親子関係が破綻してしまいます。そのため、値を書き換えても相互にデータの書き換えを行なうことができません。
データの書き換えや参照が思ったようにうまくいかない場合は、このことを覚えておくとよいかもしれません。
まとめ
今回は、ControllerとScopeの基礎を解説しました。
特にスコープについては複雑な概念や実装になっていますので、いきなり全てを把握することは難しいです。AngularJS の経験を積むとともに徐々に学んでいきましょう。
次回はサービスについての解説を予定しています。