nkjmkzk.net

powered by Kazuki Nakajima

AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ

Part6の続きです。

今回はデータ更新後に、現在画面に表示されている情報を最新状態にアップデートする仕組みを「Promise/Deferred」なるデザインパターンを適用して実装していきます。

まず今回開発する部分をデモでみてみましょう。

デモ

それでは実装へ。

Promise/Deferredって何ですか?

データ更新後にサイドバーの情報をリフレッシュするのは難しいことではありません。

updateGuest()の成功時コールバックでgetGuests()を実行すればいいだけです。

ただし、このコールバックで逐次処理を記載していくと、いくつかの問題が発生してきます。

  • どんどんコールバックが深くなっていく(いわゆるコールバック地獄)。
  • ゲスト情報は更新したいけどその後の処理は必要ない、といったケースがでたときに更新処理だけを実施できない。

今回のようにAjax非同期通信でデータアクセスをおこなう場合、処理を並行ではなく逐次(あれが終わったらコレ。コレが終わったらソレ、という処理)でおこないたいというケースは今後どんどんでてくると思います。

そんなときに便利なのがPromise/Deferredです。

Promise/Deferredはひとつのデザインパターンであり、その実装方式としてAngularJSでは$qという機能が提供されています。Promise/Deferredを用いるとコードをネストせずに必要な非同期処理をケース・バイ・ケースでつなげていくことができます。

このPromise/Deferredを実装するには2つの作業が必要です。

  • 非同期処理をPromise/Deferred対応した関数に仕立て上げる
  • 非同期処理を実行する流れを記述する

なるほど。実際にコードをPromise/Deferredに対応させて理解していきましょう。

 

getGuests()をPromise/Deferred対応に書き換える

getGuests()関数をdeferredGetGuests()として下記のように改変します。

		$scope.deferredGetGuests = function(){
			var deferred = $q.defer();
			var soql = "select Id, Name, CreatedDate from guest__c";
			$scope.force.query(
				soql,
				function(result){
					deferred.resolve(result.records);
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;	
		}

 

ひとつずつみていきましょう。

 

			var deferred = $q.defer();

Promise/Deferred対応させる関数には決まりごととして最初に$q.defer()を実行してdeferredインスタンスを作成します。

 

					deferred.resolve(result.records);

resolve()は成功時に返す変数を指定します。今回は取得したゲストリストを返しています。

 

					deferred.reject(result);

reject()は失敗時に返す変数を指定します。

 

			return deferred.promise;	

これも決まりごとです。Promise/Deferred対応させる関数は処理の最後にdeferred.promiseを返す必要があります。

次に実行される処理はこのpromiseを受け取って処理を開始します。promiseは文字通りPromise/Deferred対応関数の処理が完了したことを保証し、その結果を渡してくれます。

これでgetGuests()はdeferredGetGuest()として生まれ変わりました。従来はこのgetGuests()はゲストリストを取得して$scope.guestsにセットするまでを担っていましたが、現在は純粋にゲストリストを呼び出し元に返すのみとなりました。したがってこのgetGuests()を呼んでいたコードをそれに対応させて修正する必要があります。

 

		$scope.deferredGetGuests()
		.then(
			function(guests){
				$scope.guests = guests;
			},
			function(result){
				console.log(result);
			}
		);

前述の通り、Promise/Deferred対応した関数はdeferred.promiseを返します。このdeferred.promiseはthen()というインスタンスメソッドを持っています。then()は第一引数に成功時のコールバック、第二引数にエラー時のコールバックをとります。(第三引数もありますが今回は割愛します)

ここでは成功時のコールバックで$scope.guestsにdeferredGetGuests()が取得してきたゲストリストをセットしています。

これで結果的に変更前と同じ挙動になりました。

これだけだと何のためにこの複雑な変更をおこなったのかよくわかりませんね。おそらく次のupdateGuest()をPromise/Deferred対応させていくと少しずつわかってくるのではないかと思います。

 

updateGuest()をPromise/Deferred対応に書き換える

同じようにupdateGuest()をPromise/Deferred対応させたdeferredUpdateGuest()として改変します。

		$scope.deferredUpdateGuest = function(){
			var deferred = $q.defer();
			var guest = angular.copy($scope.guest);
			delete guest.attributes;

			$scope.force.update(
				"guest__c",
				$scope.guest.Id,
				guest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

仕組みは先ほどのdeferredGetGuests()と同じです。deferredUpdateGuest()は何も返さないのでresolve()は何も指定していませんが、処理完了後に次の処理がおこなわれる挙動は同じです。

次にそもそもng-click()で更新ボタンクリック時によばれていたupdateGuest()を違う形で復活させます。

前回作成したモーダルウィンドウの表示に加えて、更新ボタンがクリックされた後に必要な処理の流れをすべてこのupdateGuest()に記述していきます。

 

		$scope.updateGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

			$scope.deferredUpdateGuest()
			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})
			.then(
			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});
		}

今回行う処理は下記の2つです。

  • レコード更新
  • ゲストリストのリフレッシュ(ゲストリストを再度取得することで実現します)

両方非同期処理ですが、すでに両方の関数がPromise/Deferred対応しているので同期的に記述することができます。

 

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

まず一つ目のレコード更新処理の前にremotingProgressを33にセットしています。

 

			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})

更新処理完了後の処理をthen()でつなげています。この中でremotingProgressを66に進めつつ、次の処理であるゲストリスト取得を実施しています。

そしてその結果はreturnで返してあげます。そうするとこのthen()のあとにさらにthen()をつなげて2番目の処理を記述できます。deferredGetGuest()で取得したデータは次のthen()に渡されます。

もし更新処理が失敗した場合は$q.reject(result)が実行され、その結果が次のthen()に渡されます。$q.reject()を実行すると次のthen()においても処理が失敗したものとしてみなされ、第二引数のエラー時コールバックが実行されることになります。

 

			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});

remotingProgressを100にし、受け取ったゲストリストを$scope.guestsにセットしています。

これで更新ボタンをクリックされた後に必要な一連の処理が記述できました。

$scope.guestsとサイドバーに表示されているゲストの一覧はバインドされているので、$scope.guestsを更新するだけで表示も更新されます。

この$qによるPromise/Deferredをマスターすれば、Ajax非同期通信を多様するHTML5アプリにおいても、より可読性と拡張性の高いコードが記述できるようになるはずです。

次回はモーダルダイアログを利用したレコード新規作成機能を実装していきます。

例によって現時点でのindexページの全ソースを掲載しておきます。

<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" >
  
<html ng-app="ngbootcamp">
<head>
	<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/css/bootstrap.min.css"></link>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/js/bootstrap.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.10/angular.min.js"></script>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.min.js"></script>
	<c:RemoteTK />
	<script>
	var ngbootcamp = angular.module('ngbootcamp', ['ui.bootstrap']);

	ngbootcamp.controller('guestCtl', function($scope, $modal, $q){
		$scope.updateGuest = function(){
			$modal.open({
				templateUrl: "T_inProgress",
				backdrop: "static",
				scope: $scope
			});

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "データを更新しています...";

			$scope.deferredUpdateGuest()
			.then(
			function(){
				$scope.remotingProgress = 66;
				$scope.remotingStatus = "ゲストリストをリフレッシュしています...";
				return $scope.deferredGetGuests();
			},
			function(result){
				return $q.reject(result);
			})
			.then(
			function(guests){
				$scope.guests = guests;
				$scope.remotingProgress = 100;
				$scope.remotingStatus = "更新が完了しました。";
			},
			function(result){
				console.log(result);
			});
		}

		$scope.deferredUpdateGuest = function(){
			var deferred = $q.defer();
			var guest = angular.copy($scope.guest);
			delete guest.attributes;

			$scope.force.update(
				"guest__c",
				$scope.guest.Id,
				guest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$scope.getGuest = function(recordId){
			$scope.force.retrieve(
				"guest__c",
				recordId,
				"Id,Name,email__c",
				function(result){
					$scope.guest = result;
					$scope.$apply();
				},
					function(result){
					console.log(result);
				}
			);
		}

		$scope.deferredGetGuests = function(){
			var deferred = $q.defer();
			var soql = "select Id, Name, CreatedDate from guest__c";
			$scope.force.query(
				soql,
				function(result){
					deferred.resolve(result.records);
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;	
		}

		$scope.force = new remotetk.Client();
		$scope.deferredGetGuests()
		.then(
			function(guests){
				$scope.guests = guests;
			},
			function(result){
				console.log(result);
			}
		);
	});
	</script>
</head>
<body ng-controller="guestCtl">
	<div class="container" style="margin-top:20px;">
		<div class="row">
			<div class="col-md-4">
				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
					</div>
					<div class="list-group">
						<a class="list-group-item" href="#" ng-click="getGuest(guest.Id)" ng-repeat="guest in guests">{{guest.Name}}</a>
					</div>
				</div>
			</div>
			<div class="col-md-8">
				<h1>{{guest.Name}}</h1>
				<form role="form">
					<div class="form-group">
						<label>ゲスト名</label>
						<input ng-model="guest.Name" type="text" class="form-control" placeholder="ゲスト名" />
					</div>
					<div class="form-group">
						<label>Email</label>
						<input ng-model="guest.email__c" type="email" class="form-control" placeholder="Email" />
					</div>
					<div class="form-group">
						<button class="btn btn-success" ng-click="updateGuest()">更新</button>
					</div>
				</form>
			</div>
		</div>
	</div>

	<!-- Modal for inProgress -->
	<script type="text/ng-template" id="T_inProgress">
		<div class="modal-header">
		    <h3>
		    	<span ng-show="remotingProgress < 100">処理中</span>
		    	<span ng-show="remotingProgress == 100">完了</span>
	    	</h3>
		</div>
		<div class="modal-body">
			<div>{{remotingStatus}}</div>
		    <progressbar ng-class="(remotingProgress < 100) ? 'progress-striped active' : 'progress'" value="remotingProgress" type="success"></progressbar>
		</div>
		<div class="modal-footer" ng-show="remotingProgress == 100">
			<button type="button" class="btn btn-success" ng-click="$close()">閉じる</button>
		</div>
	</script><!-- Modal for inProgress-->

</body>
</html>
  
</apex:page>

 

関連情報

without comments

Written by 中嶋 一樹

2月 16th, 2014 at 11:49 am

Leave a Reply