実践!AngularJS 前編 Controller間で値を共有 1
AngularJSを多少使い始めた人がぶつかりがちな問題を、さまざまな方法で解決していきます。今回は$scopeオブジェクトを使い、コントローラー間で値を共有する様子を見てみましょう。
- カテゴリー
- JavaScript >
- Angular
発行
はじめに
このシリーズは、AngularJSを使うときにぶつかりやすい問題について、さまざまな解決方法を紹介するものです。ある程度AngularJSを理解している人に向けた記事ですので、AngularJSについて基本から知りたいという人は、CodeGridの過去のシリーズ「攻略!AngularJS」を参照してください。
AngularJSを用いて、ある程度の規模の開発をした場合、あるコントローラーの中に別のコントローラーを作る機会がよくあると思います。そのときに、親のコントローラーと子のコントローラーの間で値を共有したいと思うことがあるでしょう。本記事では、コントローラー間で値を共有する方法について、いくつかのパターンを紹介していきます。
なお、このシリーズで紹介するサンプルは次のリポジトリにまとめてあります。併せて参照してください。
実践!AngularJSサンプルリポジトリ
$scopeオブジェクトを使う
コントローラー間で値を共有する方法でまず始めに思い浮かぶのは、$scope
オブジェクトを使って値を共有することだと思います。AngularJSでは、親のコントローラーの$scope
に定義されたプロパティは、子のコントローラーへと継承されます。
その仕組みを使えば簡単に値の共有を行えるように思えるかもしれませんが、実はここには落とし穴があります。次のような条件で簡単なフォームを作ってみましょう。
- 親のコントローラーのinput要素に入力した値が、子のコントローラーのinput要素へ反映される
- 子のコントローラーのinput要素へ値を入力すると、親のコントローラーのinput要素に反映される
$scope
を使って作ったものが、以下のデモです。
しかし、実際にこのデモを動かしてみると、意図した通りに動いてくれません。親のinput要素から値を入力すると、子のinput要素へと変更が反映されます。ところが、子のinputに値を入力しても、親のinputへと変更が反映されません。さらに、一度子のinput要素に値を入力してしまうと、その後親のinput要素で値を変更しても、子のinput要素へと変更が反映されなくなってしまいます。
なぜこのようなことが起こってしまうのでしょうか。
$scopeの継承の仕組みを知る
この現象の原因を探るためには、$scope
の継承の仕組みを知る必要があります。まずはこのデモのソースコードを見てみましょう。
var app = angular.module('app', []);
app.controller('ParentCtrl', function($scope) {
$scope.message = 'Hello World!!'
});
app.controller('ChildCtrl', function($scope) { });
<div ng-controller="ParentCtrl" class="parent">
<h2>ParentCtrl</h2>
Message here
<input type="text" ng-model="message">
<div ng-controller="ChildCtrl" class="child">
<h3>ChildCtrl</h3>
Change parent message<br>
<input type="text" ng-model="message">
</div>
</div>
コントローラーParentCtrl
の$scope
にmessage
という名前でプロパティが定義されています。テンプレートでは、ParentCtrl
で定義したmessage
プロパティを、ParentCtrl
・ChildCtrl
が持つそれぞれのinput要素のng-model
属性値にセットしています。一見すると意図したとおりに動きそうですが、そうはなりません。
コントローラーが$scope
を継承するとき、親の$scope
に定義されたプロパティは、子の$scope
のprototypeへと格納されます。このとき、子の$scope
に存在しないプロパティを参照すると、自分自身のprototypeからそのプロパティを探そうとします。
つまり、ChildCtrl
のinput要素がmessage
プロパティを取得する際、自分自身にはmessage
プロパティが存在していないため、prototypeに格納されたParentCtrl
の$scope
を参照します。そのため、「親のinput要素を変更すると子のinput要素へと変更が反映される」ということが可能になるのです。
一方、ChildCtrl
のinput要素から変更した場合はどうでしょう。この場合、入力が行われた子のinput要素のng-model
により、ChildCtrl
の$scope
に新しく同名のプロパティ(この場合はmessage
)が作られます。この時点で、ChildCtrl
の$scope
から見ると、message
プロパティは自分自身が持っていることになるので、ParentCtrl
の$scope
が格納されているprototypeへの参照を切ってしまいます。
そうなると、ParentCtrl
とChildCtrl
の$scope
は、お互いに値を受け取ることも値を取り出すこともできなくなってしまい、「子のinput要素で変更しても親のinput要素に反映されない」「一度でも子のinput要素で変更すると、親のinput要素で変更しても子に反映されない」という現象が起きてしまうのです。
この現象は、コントローラー間に限った話ではなく、ディレクティブを作る際にscope
プロパティにtrue
を指定した場合でも起こり得るので、注意が必要です。
JavaScriptのprototype継承
JavaScriptにおいて、あるオブジェクトが特定のプロパティを探すとき、まず自分自身からそのプロパティを探し、見つからなかった場合は自分自身のprototypeに同名のプロパティがあるかを探します。それでも見つからない場合はprototypeのprototype……prototypeのprototypeのprototype……と探していき、最終的に見つからない場合はundefinedとなります。
AngularJSの$scope
でも、これと同じことが行われています。
中間オブジェクトを使って継承させる
これらのことを踏まえ、次のデモではどちらのinputを編集してもお互いに変更が反映されるようにしました。ParentCtrl
の一部を修正しています。
ソースコードを見てみましょう。どこが変わったのでしょうか。
app.controller('ParentCtrl', function($scope) {
// 中間オブジェクトを作ってプロパティを紐付ける
$scope.vm = {};
$scope.vm.message = 'Hello World!!'
});
<div ng-controller="ParentCtrl" class="parent">
<h2>ParentCtrl</h2>
Message here
<input type="text" ng-model="vm.message">
<div ng-controller="ChildCtrl" class="child">
<h3>ChildCtrl</h3>
Change parent message<br>
<input type="text" ng-model="vm.message">
</div>
</div>
最初の動かなかった例と比べて変更しているのは、一箇所だけです。動かなかった例では、$scope
に直接message
プロパティを定義していましたが、今度はvm
というオブジェクトを間に挟んでいます。こうすると、input要素によって変更されるのは、vm
オブジェクトの中のmessage
プロパティとなり、vm
オブジェクトそのものは書き換わらなくなります。
これにより、ChildCtrl
の$scope
の中に新しくvm
というオブジェクトが作られることもなくなり、prototypeへの参照が残り続け、意図した通りに親子のコントローラー間で値の共有を行えるようになりました。
Controller As記法を使う
AngularJS 1.2からController As記法が使えるようになっています。これは、$scope
を用いる場合に$scope
オブジェクトにプロパティを定義していた代わりに、コントローラーのthis
に各プロパティを定義していき、ng-controller
で任意の変数にコントローラーのインスタンスを格納してテンプレートで使えるようにする、というものです。
このController As記法を使って、コントローラー間の値を共有してみましょう。まずはデモを見てみます。
前のデモと同様に、親と子のinput要素のどちらで入力しても、同じ内容がそれぞれのinput要素に表示されます。コードを見てみましょう。
app.controller('ParentCtrl', function($scope) {
this.message = 'Hello World!!'
});
app.controller('ChildCtrl', function($scope) { });
<div ng-controller="ParentCtrl as parent" class="parent">
<h2>ParentCtrl</h2>
Message here
<input type="text" ng-model="parent.message">
<div ng-controller="ChildCtrl as child" class="child">
<h3>ChildCtrl</h3>
Change parent message<br>
<input type="text" ng-model="parent.message">
</div>
</div>
JavaScriptのコードでは、ParentCtrl
のthis
にmessage
というプロパティを作り、文字列を代入しています。ここはあまり大きく変わっていません。
一方、HTMLでは、前掲のデモのコードでng-controller="[コントローラー名]"
となっていたところが、ng-controller="[コントローラー名] as [変数名]"
となっています。このように書くことで、ParentCtrl
のインスタンスが変数parent
へ、ChildCtrl
のインスタンスが変数child
へ格納され、それぞれの変数からそれぞれのコントローラーのthis
に定義したプロパティ/メソッドを参照することができるようになります。
input要素のng-model
属性値も、親子ともにparent.message
となっています。$scope
を用いた場合は、コントローラーの中では$scope
に定義(もしくは継承)されたプロパティを使わざるを得ませんでしたが、Controller As記法を用いた場合では、任意のコントローラーが持っているプロパティそのものを参照することができます。
$scope
を用いた場合では、継承により意図しないプロパティの参照や上書きなどが発生する可能性があります。一方、Controller As記法を用いた場合では、それぞれのコントローラーで独立したプロパティを定義できるため、そういった懸念が必要なく、メンテナンス性を向上できるなどのメリットがあります。
ただ、今回のデモは値を受け取って表示するだけでしたが、parent.message
の値をChildCtrl
で受け取って加工したいなどといった場合は、値を受け取るメソッドを用意したり、次回で解説するサービスを利用するなど工夫が必要になります。
まとめ
今回はAngularJSのコントローラー間で値を共有するという観点から、$scope
の仕組みと気を付けたい落とし穴について解説しました。また、各コントローラーで独立したプロパティを定義できる、Controller As記法を使った値の共有も解説しました。
次回は、イベントやサービスを使った値の共有を解説します。