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

