【まとめ】AngularJS でデータ共有用のファクトリーを生成し、複数のコントローラーに注入したときのふるまい・ポイント
- ファクトリーオブジェクトは 1 つのみ存在する。
- $scope はファクトリーの参照である。
- ファクトリーまたは $scope のどちらのプロパティの値を変更しても、すべてのコントローラーのデータが即時で同期される
- ファクトリーまたは $scope のどちらにプロパティを追加しても、すべてのコントローラーのデータが即時で同期される
はじめに
- Monaca で iOS アプリを作っている
- 一覧ページと詳細ページがあり、それぞれ別の AngularJS のコントローラーが紐付いている
- 一覧ページで選択したデータは、移動先の詳細ページで値を更新できる
- 詳細ページで更新した値は一覧ページの値へも反映されていてほしい
こんなことを実現したいのですけれども、コントローラー間でデータの共有ってできるのかしら?
コントローラー内では $scope を通じて、双方向バインドが実現できます。フォームに入力した即時に表示されるイメージです。別のコントローラへも同様に反映できないかしら?
簡単で単純な例から少しずつ調べていきたいと思います。
レベル1. 同じページに複数のコントローラー
調べてみましたら、とても良い例を見つけました。
まずは同じことを、Onsen UI を使用した Monaca を使った iOS アプリで実現してみます。
アプリの構成
- プロジェクト名: SyncSample1
- テンプレート: Onsen UI最小限のテンプレート
- index.html、page1.html、page2.html と 3 ファイルに分かれているが、SPA(Single Page Application)となるように index.html にまとめる。
作るものの簡単な仕様
- それぞれのコントローラーに 1 つずつの入力フォーム。
- それぞれの 入力フォームには $scope をバインド
- 入力フォームの値を編集すると、もう一つのコントローラーの入力フォームに即時に反映される。
コードは、次のようになりました。
index.html
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <script src="components/loader.js"></script> <link rel="stylesheet" href="components/loader.css"> <link rel="stylesheet" href="css/style.css"> <script> var app = ons.bootstrap(); app.factory("SharedStateService", function () { return { text: 'SharedStateService' }; }); app.controller("ShareControllerA", function ($scope, SharedStateService) { $scope.data = SharedStateService; }); app.controller("ShareControllerB", function ($scope, SharedStateService) { $scope.data = SharedStateService; }); app.controller("ShareControllerC", function ($scope, SharedStateService) { $scope.data = SharedStateService; }); </script> </head> <body> <ons-navigator var="myNavigator" page="page1.html"> </ons-navigator> <ons-template id="page1.html"> <ons-page> <ons-toolbar> <div class="center">Navigator</div> </ons-toolbar> <h4>Shared State Service</h4> <div ng-controller="ShareControllerA"> <h5>This is ShareControllerA</h5> <input type="text" ng-model="data.text"> </div> <div ng-controller="ShareControllerB"> <h5>This is ShareControllerB</h5> <input type="text" ng-model="data.text"> </div> <div ng-controller="ShareControllerC"> <h5>This is ShareControllerC</h5> <input type="text" ng-model="data.text"> </div> </ons-page> </ons-template> </body> </html>
試したところ、入力フォームの値を変更すると、ShareControllerA、B、C の入力フォームの値がすべて同じように即時に変化し、同期していることが確認できました!
SharedStateService ファクトリーを通して、データが共有されています。
それぞれのコントローラーの $scope.data に SharedStateService をバインドしています。SharedStateService にはプロパティ text がございます。
ですから、$scope.data と SharedStateService の間を text が行き来しているというイメージかしら。
HTML では data.text で入力を受け付けております。そのことを考えると、
HTML の data.text ←→ 各コントローラーの $scope.data ←→ ファクトリー SharedStateService
とつながっているのだと思います。
レベル2. 異なるページに複数のコントローラー
- iOS アプリのページ(画面)遷移がある
- それぞれのページに入力フォームを設ける
- ボタンをタップしたら、プロパティの値を変更する関数を設ける
- ボタンをタップしたら、プロパティを追加する関数を設ける
以上がレベル 1 とは異なる点ですわ♪逆に、
- どの入力フォームで値を編集しても同期する
- どの入力フォームも属するコントローラーが異なる
以上の点は同じですの。
index.html
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <script src="components/loader.js"></script> <link rel="stylesheet" href="components/loader.css"> <link rel="stylesheet" href="css/style.css"> <script> var app = ons.bootstrap(); app.factory("SharedStateService", function () { return { text: 'SharedStateService' }; }); app.controller("ShareControllerA", function ($scope, SharedStateService) { $scope.data = SharedStateService; $scope.changeText = function () { $scope.data.text = "$scope.data.text を変更"; }; $scope.changeText2 = function () { SharedStateService.text = "SharedStateService.text を変更" }; $scope.showPlus = function () { $scope.data.plus = "$scope.data に plus を新しいプロパティとして追加"; } $scope.showPlus2 = function () { SharedStateService.plus2 = "SharedStateService に plus2 を新しいプロパティとして追加"; } }); app.controller("ShareControllerB", function ($scope, SharedStateService) { $scope.data = SharedStateService; }); app.controller("ShareControllerC", function ($scope, SharedStateService) { $scope.data = SharedStateService; }); </script> </head> <body> <ons-navigator var="myNavigator" page="page1.html"> </ons-navigator> <ons-template id="page1.html"> <ons-page> <ons-toolbar> <div class="center">Navigator</div> </ons-toolbar> <!-- 異コントローラー間 $scope 共有に関係有るのはココから --> <h4>Shared State Service</h4> <div ng-controller="ShareControllerA"> <h5>This is ShareControllerA</h5> <input type="text" ng-model="data.text"> <ons-button ng-click="changeText()"> changeText </ons-button> <ons-button ng-click="changeText2()"> changeText2 </ons-button> <ons-button ng-click="showPlus()"> showPlus </ons-button> <p>{{data.plus}}</p> <ons-button ng-click="showPlus2()"> showPlus2 </ons-button> <p>{{data.plus2}}</p> </div> <div ng-controller="ShareControllerB"> <h5>This is ShareControllerB</h5> <input type="text" ng-model="data.text"> <p>{{data.plus}}</p> <p>{{data.plus2}}</p> </div> <!-- 異コントローラー間 $scope 共有に関係有るのはココまで --> <div style="text-align: center"> <br> <ons-button onclick="myNavigator.pushPage('page2.html')"> Push Page 2 </ons-button> </div> </ons-page> </ons-template> <ons-template id="page2.html"> <ons-page> <ons-toolbar> <div class="left"><ons-back-button>Back</ons-back-button></div> <div class="center">Page 2</div> </ons-toolbar> <!-- 異コントローラー間 $scope 共有に関係有るのはココから --> <div ng-controller="ShareControllerC"> <h5>This is ShareControllerC</h5> <input type="text" ng-model="data.text"> <p>{{data.plus}}</p> <p>{{data.plus2}}</p> </div> <!-- 異コントローラー間 $scope 共有に関係有るのはココまで --> <div style="text-align: center"> <h1>Page 2</h1> <ons-button onclick="myNavigator.popPage()"> Pop Page </ons-button> </div> </ons-page> </ons-template> </body> </html>
今回も、どの入力フォームで編集しても、また、ボタンでデータを変更しても、即座にすべての他の入力フォームへと反映されることを確認いたしました♪
ShareControllerC 部分を page2.html へと移動しておりますが、簡単な変更ですの。今回のコード修正での目玉は、なんといっても追加した関数ですわ!
プロパティの値を変更する方法 2 つ
まずは、データプロパティを変更する関数です。
$scope.changeText = function () { $scope.data.text = "$scope.data.text を変更"; }; $scope.changeText2 = function () { SharedStateService.text = "SharedStateService.text を変更" };
データを変更する方法として、今まで HTML 側の ng-model に割り当てた data.text が出入口となっておりました。
では、JavaScript 側、コントローラー内でデータを変更するにはどうすればよいでしょうか?候補としては 2 つ考えられます。
- $scope.data.text
- SharedStateService.text
changeText 、changeText2 関数を作って確かめてみますと、ファクトリーのプロパティを変更しても、$scope のプロパティの値を変更しても、すべてのコントローラーのデータが即時で同期されましたわ!
プロパティを追加する方法 2 つ
つづいて、プロパティを追加する関数ですの!
$scope.showPlus = function () { $scope.data.plus = "$scope.data に plus を新しいプロパティとして追加"; } $scope.showPlus2 = function () { SharedStateService.plus2 = "SharedStateService に plus2 を新しいプロパティとして追加"; }
コントローラーの最初で $scope.data = SharedStateService; としておりますので、
- $scope.data に追加する。例えば $scope.data.plus = “”;
- SharedStateService に追加する。例えば SharedStateService.plus2 = “”;
の 2 通りがございます。先程と同様に、どちらの方法でもすべてのコントローラー内の {{data.plus}} や {{data.plus2}} へ追加プロパティの値が即時に同期されましたの♪
値の変更、プロパティの追加はどの方法を使うべきか?
どちらでも正しく反映されるとなりますと、逆に迷ってしまいます。つまり、ファクトリーと $scope のどちらを使ってプロパティを変更すればよいのかしら、ということですの。
これを検証するコードが思いつきません。どちらがベターかは、AngularJS の仕組みというよりも、利用する側のアプリ設計によるような気がいたします。
ちなみに、いじわるしてみます。$scope.data.text を $scope.data に変更、つまり
$scope.data = "ボタン押して関数内で変更";
としますと正しく動きません。次のような症状が現れ、エラーも出ましたの。
- changeText ボタンタップで ShareControllerA の入力フォームのみ空白になる。
- ShareControllerA のフォームに入力すると、「Error: Attempted to assign to readonly property」とエラーになる。
- changeText ボタンタップ後に ShareControllerB または C の入力フォームを編集すると、これらの入力フォームは同期する。しかし ShareControllerA は同期せず、空白のまま。
おわりに
基本的なところは整理できたように存じます♪
「レベル3. 異なるページに複数のコントローラーでデータはオブジェクトの配列」が最終段階と考えておりますけれども、一旦はレベル 2 で筆をおきたいと思います。
- AngularJS – Angular JS で複数のコントローラ間でモデル(状態や値)を共有する方法 3 種類 – Qiita
- AngularJS ではモデルをどう宣言すればいいのか – AngularJS Ninja Blog
以上です。