【まとめ】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
以上です。

