カテゴリー
コンピューター

【AngularJS】一覧・詳細画面ごとのコントローラー間でデータ共有しているサンプル【Monaca】【Onsen UI】

一覧、詳細のコントローラー間でデータを共有する AngularJS アプリサンプル!

【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": "キャシー"
}

おわりに

こちらのページもコントローラー間でのデータ共有方法について、理解を深めるのに役に立ちました。ありがとう存じます!

以上です。

コメントを残す