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

【AngularJS 】ページにバインドした $scope を別のコントローラと共有・同期したい【Monaca 】【Onsen UI】

AngularJS でデータ共有用のファクトリーを生成し、複数のコントローラーに注入することでコントローラー間でデータ同期を取る

【まとめ】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 で筆をおきたいと思います。

以上です。

コメントを残す