【AngularJS 】ページにバインドした $scope を別のコントローラと共有・同期したい【Monaca 】【Onsen UI】 | oki2a24 の続編となります
今まで、Monaca に Onsen UI を組み込んで AngularJS を駆使して次の 2 つのデータ共有方法を学習してまいりました♪
- レベル1. 同じページに複数のコントローラー
- レベル2. 異なるページに複数のコントローラー
そして、次の点を学びました。
- ファクトリーオブジェクトは 1 つのみ存在する。
- $scope はファクトリーの参照である。
- ファクトリーまたは $scope のどちらのプロパティの値を変更しても、すべてのコントローラーのデータが即時で同期される
- ファクトリーまたは $scope のどちらにプロパティを追加しても、すべてのコントローラーのデータが即時で同期される
実際のアプリでは、一覧画面でひとつを選択して詳細画面へ行き、編集や削除を行い、一覧画面へ戻る、そのとき詳細画面の変更内容は一覧画面へ反映済み、という動きが普通と存じます。
今回は、そのようなケースを想定して次のような仕様のサンプルを作ります。
- 一覧画面: 一覧データはサーバから取得してメモリに保存(ただしサンプルなのでダミーの JSON ファイルを作って使う)
- 一覧画面 → 詳細画面の移動時: サーバから詳細データを取得してメモリに保存(ただしサンプルなのでダミーの JSON ファイルを作って使う)
- 詳細画面: 内容を変更して「変更」ボタンをタップしたら、サーバへ内容を送信し、詳細メモリ・一覧メモリの内容も更新する。(ただしサンプルなのでサーバへ変更内容は送信しない)
ネットワーク通信は実際には行いません。一覧から詳細への移動時は毎回ダミーの JSON ファイルからデータを取得いたします。したがいまして、詳細画面が表示される時は毎回同じデータです。
レベル3. 異なるページに複数のコントローラーでデータはオブジェクトの配列
ファイル構成
json ディレクトリに JSON ファイルを置いております。これらは REST 通信をしたときに取得される役割の、ダミーデータでございます。サンプルアプリなので作りましたけれども、本来であればサーバから送られてくるデータですので不要ですの。
- index.html
- js/
- app.js
- json/
- note.json
- page0.json
- page1.json
- page2.json
index.html
一覧画面は list.html、詳細画面は detail.htm ですけれども、index.html に 2 つとも書いています。<ons-template id=”list.html”> といった部分です。これによって SPA(Single Page Application)を実現しております。
<!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> <script src="js/app.js"></script> <link rel="stylesheet" href="components/loader.css"> <link rel="stylesheet" href="css/style.css"> </head> <body> <ons-navigator var="navi" page="list.html"> </ons-navigator> <ons-template id="list.html"> <ons-page> <ons-toolbar> <div class="center">一覧</div> </ons-toolbar> <div ng-controller="ListController"> <ons-list> <ons-list-item modifier="chevron" ng-repeat="page in note" ng-click="goDetail(page.id)"> {{page.text}} </ons-list-item> </ons-list> </div> </ons-page> </ons-template> <ons-template id="detail.html"> <ons-page> <ons-toolbar> <div class="left"><ons-back-button>Back</ons-back-button></div> <div class="center">詳細</div> </ons-toolbar> <div ng-controller="DetailController"> <ons-list modifier="inset"> <ons-list-item> <ons-row> <ons-col>{{page.id}}</ons-col> <ons-col>{{page.user}}</ons-col> </ons-row> </ons-list-item> <ons-list-item> <textarea class="textarea--transparent" placeholder="テキスト" style="width: 100%; height: 300px" ng-model="page.text"></textarea> </ons-list-item> <ons-list-item> <input type="text" class="text-input--transparent" placeholder="その他" style="width: 100%" ng-model="page.other" /> </ons-list-item> </ons-list> <div style="padding: 10px 9px"> <ons-button modifier="large" ng-click="updatePage()">更新</ons-button> </div> </div> </ons-page> </ons-template> </body> </html>
js/app.js
コントローラーで共有するデータは、NoteModel と PageModel と名づけたファクトリーですの。
NoteModel が一覧画面データ、PageModel が詳細画面データですわ。
最初は、NoteModel の一覧の中に詳細データも含めることでサーバとの通信回数を減らす方針と考えておりました。けれども詳細データに画像などがあり大きなサイズの場合、一覧画面のロード中にそれらも取得するのは時間がかかってしまいます。そこで、一覧用データ、詳細用データと分け、それぞれ REST 通信して取得することといたします。
ポイント1. 共有ファクトリーでの配列の扱い
また、苦労したのは、共有するファクトリーでの配列の扱い方です。
配列を $scope に代入する時はファクトリー自身ではなくファクトリーの配列プロパティを指定する必要がございます。したがって、ファクトリーに予め配列格納用のプロパティを用意しておきます。
レベル1、レベル2ではコントローラーで
$scope.data = Factory;
とすればビューで data.property とすれば Factory.property へアクセスできました。
ところが、配列データを扱う時はこれではアクセスできず、
$scope.data = Factory.array;
とする必要がございました。
ポイント2. 共有ファクトリーへのデータの代入
また、ファクトリーにデータを代入したい時はプロパティを指定する必要があります。$scope に代入する時は不要でしたのですっかり混乱してしまいました。
具体的には、
Factory = data;
では、Factory に新しく data というプロパティが追加されてしまい Factory.data.property1 としないとアクセスできなくなるため、だめです><。
Factory .property1 = data.property1; Factory .property2 = data.property2; Factory .property3 = data.property3; ...
と一つ一つプロパティを指定する必要があります。
以上を踏まえまして、ソースは次のようになりました。
"use strict" var app = ons.bootstrap(); /** * 一覧ページで使用する「ノート」オブジェクト * 複数コントローラーでデータを共有します。 */ app.factory('NoteModel', function () { return { // pages は次のプロパティをもつオブジェクトの配列 // id: 数値、text: 文字列 pages: [], /** * NoteModel.pages プロパティの要素を更新します。 * @param {PageModel} ページオブジェクト */ updatePage: function (page) { console.log('★NoteModel.updatePage 関数スタート'); // this.pages の中から id が一致するオブジェクトを更新 for (var i = 0, max = this.pages.length; i < max; i = i + 1) { if (page.id === this.pages[i].id) { console.log('page: ' + JSON.stringify(page)); console.log('変更前 this.pages[i]: ' + JSON.stringify(this.pages[i])); this.pages[i].text = page.text; } } console.log('変更後 this.pages: ' + JSON.stringify(this.pages)); } } }); /** * 詳細ページで使用する「ページ」オブジェクト * 複数コントローラーでデータを共有します。 */ app.factory('PageModel', function () { return { id: 0, text: '', other: '', user: '' } }); /** * 一覧画面のコントローラーです。 */ app.controller('ListController', function ($scope, $http, NoteModel, PageModel) { console.log('★ListController スタート'); // 本来であれば通信してデータを取得 $http.get('json/note.json').then(function (response) { // データは他のコントローラーで使用するために、 // NoteModel の pages プロパティに代入し、 NoteModel.pages = response.data; $scope.note = NoteModel.pages; console.log('NoteModel: ' + JSON.stringify(NoteModel)); }); /** * 詳細画面へ移動します。 * @param {number} ページ の id */ $scope.goDetail = function (id) { console.log('★goDetail 関数スタート'); // ページデータを取得 // 本来であれば通信してデータを取得 $http.get('json/page' + id + '.json').then(function (response) { // 移動先のコントローラーへ渡したいため、PageModel にデータを入れる。 // 画面表示はしないため、$scope へは入れない。 // PageModel = response.data; は NG。 // 別コントローラから使用するとき、元のファクトリー(宣言した時の状態)を参照するため。 // また、PageModel.page = response.data; なども NG。 // ファクトリーに page プロパティが追加され、元々の id などには値が入らないため。 PageModel.id = response.data.id; PageModel.text = response.data.text; PageModel.other = response.data.other; PageModel.user = response.data.user; console.log('NoteModel: ' + JSON.stringify(NoteModel)); console.log('PageModel: ' + JSON.stringify(PageModel)); // 詳細画面へ移動 navi.pushPage('detail.html'); }); }; }); /** * 詳細画面のコントローラーです。 */ app.controller('DetailController', function ($scope, NoteModel, PageModel) { console.log('★DetailController スタート'); $scope.page = PageModel; console.log('NoteModel: ' + JSON.stringify(NoteModel)); console.log('PageModel: ' + JSON.stringify(PageModel)); /** * ページデータを更新します。 */ $scope.updatePage = function () { // 本来であれば通信して PageModel に対応するレコードを更新 // NoteModel を更新して、一覧に Back した時に表示が変わっているようにする。 NoteModel.updatePage(PageModel); }; });
json/note.json
一覧画面用データです。本来であれば REST 通信でサーバから取得することを想定しております。ですので、ダミーデータですわ♪
[ { "id": 0, "text": "今日は晴れ○" }, { "id": 1, "text": "昨日は曇り■" }, { "id": 2, "text": "明日は雪★" } ]
json/page0.json
詳細画面用データその 1 です。こちらもダミーデータで、page1.json、page2.json も同様ですの。
{ "id": 0, "text": "今日は晴れ○", "other": "暑かった!", "user": "アン" }
json/page1.json
{ "id": 1, "text": "昨日は曇り■", "other": "洗濯物が乾かなかった...", "user": "ボブ" }
json/page2.json
{ "id": 2, "text": "明日は雪★", "other": "寒くなりそう。防寒しっかりと!", "user": "キャシー" }
おわりに
こちらのページもコントローラー間でのデータ共有方法について、理解を深めるのに役に立ちました。ありがとう存じます!
以上です。