nkjmkzk.net

powered by Kazuki Nakajima

Archive for the ‘bootstrap’ tag

AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応

Part8の続きです。

今回はHTML5らしいレスポンシブデザインを適用していきます。

まずは今回開発するレスポンシブデザインをデモでみてみましょう。

デモ

ゲストが選択されていない状態、選択された状態、いろんなシーンでブラウザを横方向に伸縮させてみてください。動的にデザインが切り替わります。(注:IEでは正しく動作しない可能性があります)

それでは実装へ。

 

viewportの設定

実はこのサイトはすでにレスポンシブデザインが適用されています。Bootstrapはデフォルトでレスポンシブ対応となっているからです。

モバイルサイトでおなじみの下記のメタタグを<head>配下に追記してサイトにモバイルデバイス、あるいはブラウザ幅を狭くしてアクセスしてみてください。

<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
	<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.1.0/css/bootstrap.min.css"></link>

ブラウザ幅が992px未満になるとレイアウトが2列から1列に変更されるはずです。上記viewportの設定を入れればモバイルデバイスでも快適な解像度でサイトが閲覧できますね。

ただし、レイアウトが変更されるだけでは実際には物足りません。

ゲストリストがながーーくなってきた場合、モバイルデバイスで任意のゲストをタップし、その詳細情報をみるには随分と下までスクロールするはめになります。モバイルデバイスでは任意のレコードを選択したらゲスト一覧は非表示にし、詳細情報のみ表示されるようになれば格段にユーザビリティがあがりますよね。

そしてそういうユースケースで便利なのがenquire.jsというライブラリです。enquire.jsはブラウザ幅を常に監視し、ある条件にマッチすると任意の処理を実行することができます。この仕組みを応用すれば真にレスポンシブなサイトが構築できそうです。

 

enquire.jsを読み込む

まずはenquire.jsを読み込みます。このライブラリもCDNで提供されているので下記のように読み込むだけでOKです。

<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
	<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>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/enquire.js/2.0.0/enquire.min.js"></script>
	<c:RemoteTK />

 

ブラウザ幅に応じたアクションの設定

早速条件とアクションを設定しましょう。

        enquire.register("screen and (max-width:991px)", {
            match : function(){
            	$scope.device = 'sm';
            	$scope.$apply()
            },
            unmatch : function(){
            	$scope.device = 'md';
            	$scope.$apply();
            }
        });

ブラウザ幅は991px以下の場合にmatchに指定した処理が、992px以上になった場合にunmatchに指定した処理が実行されます。

ここでは$scope.deviceにsm(small、モバイルデバイスと仮定)またはmd(medium)をセットしています。

 

HTMLマークアップをブラウザ幅に応じて表示/非表示を切り替える

まず、モバイルデバイスかつゲストが選択されたときにゲストリストを非表示にする設定です。

			<div class="col-md-4" ng-hide="device == 'sm' && guest != null">
				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</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>

ng-hide (no-showの逆の効果。条件が真になると要素を非表示にします)で、前述の条件を設定します。

 

次に、モバイルデバイスかつゲストが選択されていないときにはゲストの詳細を表示する必要がないのでこれも非表示にしましょう。

			<div class="col-md-8" ng-hide="device == 'sm' && guest == null">
				<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 class="col-md-8" ng-hide="device == 'sm' && guest == null">
				<div ng-show="device == 'sm' && guest != null" style="margin-bottom:20px;">
					<button type="button" class="btn btn-default btn-block" ng-click="guest = null">
						<span class="glyphicon glyphicon-chevron-left"></span>&nbsp; ゲスト一覧へ
					</button>
				</div>
				<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>

このボタンをクリックするとng-clickに指定してある通り、guestがクリアされます。これによってゲストが選択されていない状態となり、ゲストリストの表示に切り替わります。

さて、動作を確認してみてください。モバイルデバイスで表示した際には最小限の情報のみが出力されるのはもちろん、PCのブラウザでもブラウザ幅を変更するとリアルタイムにレイアウトと表示される情報が切り替わります。

また、前回までに作成したモーダルダイアログも適切に表示されます。これは元々Bootstrapがレスポンシブに対応している恩恵ですね。

例によってこれまでに作成したindexファイルの全ソースを掲載しておきます。

<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" >
  
<html ng-app="ngbootcamp">
<head>
	<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
	<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>
	<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/enquire.js/2.0.0/enquire.min.js"></script>
	<c:RemoteTK />
	<script>
	var ngbootcamp = angular.module('ngbootcamp', ['ui.bootstrap']);

	ngbootcamp.controller('guestCtl', function($scope, $modal, $q, $timeout){
		$scope.openNewGuestForm = function(){
			$scope.newGuest = {};
			$modal.open({
				templateUrl: "T_newGuestForm",
				scope: $scope
			});
		}

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

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "ゲストを作成しています...";

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

		$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.deferredCreateGuest = function(){
			var deferred = $q.defer();

			$scope.force.create(
				"guest__c",
				$scope.newGuest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$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);
			}
		);

        enquire.register("screen and (max-width:991px)", {
            match : function(){
            	$scope.device = 'sm';
            	$scope.$apply()
            },
            unmatch : function(){
            	$scope.device = 'md';
            	$scope.$apply();
            }
        });
	});
	</script>
</head>
<body ng-controller="guestCtl">
	<div class="container" style="margin-top:20px;">
		<div class="row">
			<div class="col-md-4" ng-hide="device == 'sm' && guest != null">
				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</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" ng-hide="device == 'sm' && guest == null">
				<div ng-show="device == 'sm' && guest != null" style="margin-bottom:20px;">
					<button type="button" class="btn btn-default btn-block" ng-click="guest = null">
						<span class="glyphicon glyphicon-chevron-left"></span>&nbsp; ゲスト一覧へ
					</button>
				</div>
				<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 newGuestForm -->
	<script type="text/ng-template" id="T_newGuestForm">
		<div class="modal-header">
			<button type="button" class="close" ng-click="$dismiss()">&times;</button>
		    <h3>新規ゲスト</h3>
		</div>
		<div class="modal-body">
			<form role="form">
				<div class="form-group">
					<label>ゲスト名</label>
					<input ng-model="newGuest.Name" type="text" class="form-control" placeholder="ゲスト名" />
				</div>
				<div class="form-group">
					<label>Email</label>
					<input ng-model="newGuest.email__c" type="email" class="form-control" placeholder="Email" />
				</div>
			</form>
		</div>
		<div class="modal-footer">
			<button type="button" class="btn btn-success" ng-click="createGuest()">作成</button>
		</div>
	</script><!-- Modal for newGuestForm-->

	<!-- 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月 20th, 2014 at 1:15 pm

AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム

Part7の続きです。

今回は新規ゲスト作成フォームを実装していきます。Part6で登場したモーダルダイアログ、そしてPart7で登場したPromise/Deferredパターンを駆使していきます。

new_guest_form

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

デモ

それでは実装へ。

 

新規ゲストフォームのモーダルダイアログを作成する

まずモーダルダイアログとなるHTMLマークアップを作成しておきましょう。

	<!-- Modal for newGuestForm -->
	<script type="text/ng-template" id="T_newGuestForm">
		<div class="modal-header">
			<button type="button" class="close" ng-click="$dismiss()">&times;</button>
		    <h3>新規ゲスト</h3>
		</div>
		<div class="modal-body">
			<form role="form">
				<div class="form-group">
					<label>ゲスト名</label>
					<input ng-model="newGuest.Name" type="text" class="form-control" placeholder="ゲスト名" />
				</div>
				<div class="form-group">
					<label>Email</label>
					<input ng-model="newGuest.email__c" type="email" class="form-control" placeholder="Email" />
				</div>
			</form>
		</div>
		<div class="modal-footer">
			<button type="button" class="btn btn-success" ng-click="createGuest()">作成</button>
		</div>
	</script><!-- Modal for newGuestForm-->

ほとんどのマークアップはPart6で紹介したものなので容易に理解できると思います。

各フォームがバインドするデータモデルは既存のゲストと区別するためにnewGuestとなっています。

 

モーダルダイアログを開くopenNewGuestForm()を作成する

		$scope.openNewGuestForm = function(){
			$scope.newGuest = {};
			$modal.open({
				templateUrl: "T_newGuestForm",
				scope: $scope
			});
		}

モーダルダイアログを開く部分はもうご存知の通りです。重要なのはその手前でデータモデル$scope.newGuestを初期化しているところです。

これはscopeの階層構造上必要となる処理です。

子scopeは親scopeのプロパティを参照できますが、親scopeは子scopeのプロパティにアクセスできません。

今回、openNewGuestForm()は親scopeとなり、モーダルダイアログはその子scopeとして作成されます。したがって、newGuestを親scopeで定義しておくとモーダルダイアログではそれを参照する形になりますが、親scopeでnewGuestを定義していなかった場合は、モーダルダイアログのng-modelの指定によって子scopeでnewGuestが作成されることになります。

後続の処理で親scopeがnewGuestへのアクセスを必要としますので今回は親scope側でデータモデルを作成しています。

 

新規ゲスト作成処理の流れを記述するcreateGuest()を作成する

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

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "ゲストを作成しています...";

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

ほとんどはupdateGuest()と同じ流れですね。

一点、下記の$scope.newGuestを再初期化している部分に注目してみてください。

					$scope.newGuest = {};

今回の処理ではゲスト作成処理が完了しても新規ゲストフォームはまだ表示されたままになっています。これはユースケースにもよると思いますが、連続的にデータを作成していきたい場合にはこのような仕様が適していると思います。

その際、そのままだとユーザーが入力した値がそのまま残ってしまいます。したがって一旦$scope.newGuestを初期化してフォームをクリアしているわけです。

 

deferredCreateGuest()を作成する

		$scope.deferredCreateGuest = function(){
			var deferred = $q.defer();

			$scope.force.create(
				"guest__c",
				$scope.newGuest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

この関数はPart7で学んだPromise/Deferredに対応させています。ここではRemoteTKのcreate()メソッドでデータベースにアクセスし、レコードを作成しています。

create()の第一引数はオブジェクト名、第二引数はデータです。データには$scope.newGuestをそのまま渡してあげればOKです。

 

新規ボタンを設置する

最後にサイドバーの右上に「新規」ボタンを設置しておきましょう。

コード:新規ボタン

				<div class="panel panel-default">
					<div class="panel-heading">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</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>

いわずもがな、クリックするとopenNewGuestForm()を実行してモーダルダイアログを呼び出します。

さて、随分と基本機能ができあがってきてアプリらしくなってきました。
次回はこのサイトにレスポンシブデザインを適用し、モバイルデバイスに対応させていきます。

例によって最後に現時点での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.openNewGuestForm = function(){
			$scope.newGuest = {};
			$modal.open({
				templateUrl: "T_newGuestForm",
				scope: $scope
			});
		}

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

			$scope.remotingProgress = 33;
			$scope.remotingStatus = "ゲストを作成しています...";

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

		$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.deferredCreateGuest = function(){
			var deferred = $q.defer();

			$scope.force.create(
				"guest__c",
				$scope.newGuest,
				function(result){
					deferred.resolve();
				},
				function(result){
					deferred.reject(result);
				}
			);
			return deferred.promise;
		}

		$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">
						ゲスト
						<button type="submit" class="btn btn-xs btn-default pull-right" ng-click="openNewGuestForm()"><span class="glyphicon glyphicon-plus"></span>&nbsp; 新規</button>
					</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 newGuestForm -->
	<script type="text/ng-template" id="T_newGuestForm">
		<div class="modal-header">
			<button type="button" class="close" ng-click="$dismiss()">&times;</button>
		    <h3>新規ゲスト</h3>
		</div>
		<div class="modal-body">
			<form role="form">
				<div class="form-group">
					<label>ゲスト名</label>
					<input ng-model="newGuest.Name" type="text" class="form-control" placeholder="ゲスト名" />
				</div>
				<div class="form-group">
					<label>Email</label>
					<input ng-model="newGuest.email__c" type="email" class="form-control" placeholder="Email" />
				</div>
			</form>
		</div>
		<div class="modal-footer">
			<button type="button" class="btn btn-success" ng-click="createGuest()">作成</button>
		</div>
	</script><!-- Modal for newGuestForm-->

	<!-- 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 3:43 pm

AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ

Part5の続きです。

今回は更新ボタンをクリックした後に経過を表示するプログレスバーと、重複送信を防止するモーダルウィンドウを合わせて実装していきます。

inprogress

まずデモで動作を確認してみましょう。

デモ

それでは実装へ。

UI Bootstrapを組み込む

UI BootstrapはAngular UIチームによって開発されているBootstrap用のコンポーネントライブラリです。

このライブラリを活用することにより、Bootstrapが元々提供しているプログレスバーとモーダルウィンドウをAngularJSと連携させて利用できるようになります。

このUI BootstrapもCDNで提供されているのでまずはそのファイルを読み込みます。

<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 />

 

次にUI BootstrapをAngularJSに組み込みます。

UI BootstrapはAngularJSの「モジュール」として提供されています。AngularJSではこのようなモジュールを実行時に組み込むことができるようになっています。

モジュールを組み込むには、最初のangular.module()メソッドにおいて、第二引数の配列にモジュール名を指定してあげるだけです。

	var ngbootcamp = angular.module('ngbootcamp', ['ui.bootstrap']);

これでUI Bootstrapが利用できる状態となりました。

 

モーダルウィンドウを作成する

下記のモーダルウィンドウ用HTMLマークアップを<body>タグ配下の最下部に追加します。

    <!-- 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-->

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

 

	<script type="text/ng-template" id="T_inProgress">

このscriptタグはちょっと特殊ですね。配下に書かれているのはscriptではなくHTMLマークアップです。

後にJavascriptからこのスクリプトのidを指定して$modal.open()メソッドを実行することによって、内部に書かれたHTMLマークアップがモーダルウィンドウとして表示されることになります。

 

        <div class="modal-header">
            <h3>
                <span ng-show="remotingProgress < 100">処理中</span>
                <span ng-show="remotingProgress == 100">完了</span>
            </h3>
        </div>

header部分です。no-showによって、remotingProgressの値に応じて表示される文字列を切り替えています。remotingProgressには処理の経過が0 〜 100で代入されます。100未満であればまだ処理を実行中なので「処理中」と表示し、100であれば処理が完了したということで「完了」が表示されます。

 

        <div class="modal-body">
            <div>{{remotingStatus}}</div>
            <progressbar ng-class="(remotingProgress < 100) ? 'progress-striped active' : 'progress'" value="remotingProgress" type="success"></progressbar>
        </div>

進捗状況を報告するメッセージとプログレスバーです。
remotingStatusには現在の処理が随時表示されます。
プログレスバーはUI Bootstrapが提供するカスタムディレクティブ、 で実装しています。このプログレスバーにはng-classに条件によって切り替わるクラス指定がおこなわれています。remotingProgressが100未満であればprogress-striped activeが、100であればprogressが適用されます。つまりremotingProgressが100未満であればプログレスバーは斜線がかったカラーでさらにそれが床屋の看板のようにグルグルと回っているように表示されます。100になるとそのグルグルを止めることで完了した感を演出しています。

 

		<div class="modal-footer" ng-show="remotingProgress == 100">
			<button type="button" class="btn btn-success" ng-click="$close()">閉じる</button>
		</div>

完了時に表示される「閉じる」ボタンを表示するエリアです。こちらもng-showによってremotingProgressが100になったときにはじめて表示されます。

 

updateGuest()にモーダルウィンドウを表示する仕組みを組み込む

udpateGuestメソッドに下記のようにモーダルウィンドウ用の仕組みを追加します。

        $scope.updateGuest = function(){
            var guest = angular.copy($scope.guest);
            delete guest.attributes;

            $modal.open({
                templateUrl: 'T_inProgress',
                backdrop: 'static',
                scope: $scope
            });

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

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

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

 

			$modal.open({
				templateUrl: 'T_inProgress',
				backdrop: 'static',
				scope: $scope
			});

まず$modal.open()メソッドで更新処理を開始する前にモーダルウィンドウを表示させています。$modal.open()のオプションについてみていきましょう。

  • templateUrl => モーダルウィンドウを描写するHTMLファイルを指定します。別ファイルとして作成しそのURLを指定するというのが最終的にはスマートだと思いますが、今回はこのHTMLマークアップを同一ファイル内に<script>タグで記載しています。その場合、<script>タグのid値を指定することでモーダルウィンドウのマークアップを指定することができます。
  • backdrop => モーダルウィンドウの挙動を指定します。通常、モーダルウィンドウの背景をクリックするとモーダルウィンドウが解除されてしまうのですが、今回のユースケースでは重複送信防止のため処理が完了するまでは操作をブロックしておきたいところです。その場合はこのbackdropをstaticに設定することで希望する挙動を実現できます。
  • scope => デフォルトではモーダルウィンドウには完全に切り離されたscopeが割り当てられます。今回は処理の進捗をあらわすremotingProgressおよびremotingStatusという変数を処理実行側とモーダルウィンドウ側で共有したいため、現在のscopeの子scopeを作成してそれをモーダルウィンドウに割り当てています。(子scopeは親scopeの変数にアクセスすることができます)

 

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

今回の仕組みではremotingProgressの値は最終的にプログレスバーの長さを指定することになります。今回は初期値として50を割り当てていますのでプログレスバーは50%の長さからスタートすることになります。
remotingStatusには現在の処理状況をセットします。これはそのままプログレスバーの上に表示されることになります。

 

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

更新処理成功時のコールバックでremotingProgressに100をセットし、remotingStatusに更新完了のメッセージをセットしています。これによって処理完了後にプログレスバーが100%まで伸長し、それに連動して「閉じる」ボタンが表示されることになります。

これでプログレスバーとモーダルダイアログが完成です。
 

次回はデータ更新後に画面をリフレッシュし、サイドバーに最新情報が反映される仕組みを実装していきます。

例によって最後に現時点での全ソースコードを掲載しておきます。

<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){
        $scope.updateGuest = function(){
            var guest = angular.copy($scope.guest);
            delete guest.attributes;

            $modal.open({
                templateUrl: 'T_inProgress',
                backdrop: 'static',
                scope: $scope
            });

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

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

        $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.getGuests = function(){
            var soql = "select Id, Name, CreatedDate from guest__c";
            $scope.force.query(
                soql,
                function(result){
                    $scope.guests = result.records;
                    $scope.$apply();
                },
                    function(result){
                    console.log(result);
                }
            );
        }

        $scope.force = new remotetk.Client();
        $scope.getGuests();
    });
    </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月 15th, 2014 at 4:49 pm

AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用

Part2の続きです。

今回はBootstrapを適用してアプリを現代風にイメチェンします。検証作業も見た目が質素だとやる気が削がれるので外観をある程度整えておくのはとても重要です。

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

デモ

では実装へ。

Bootstrapをインポートする

BootstrapもAngularJS同様にCDNでホスティングされているのでそちらを指定して読み込みます。BootstrapにはJavascriptライブラリも用意されていますが、こちらはjQueryを必要としますので合わせて読み込みます。

<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>
	<c:RemoteTK />

たったこれだけ。

HTMLにBootstrapを適用していく

Bootstrapには非常に多くのスタイル、そしてコンポーネントが用意されています。

グリッドシステムやサイドバー等をBootstrapに依存して構成するにはBootstrapの記法にしたがってマークアップを記述していく必要があります。

今回はまずBootstrapのグリッドシステムを利用して画面を縦2列に分割し、ゲスト一覧を左列に表示するようにしてみます。

<body ng-controller="guestCtl">
	<div class="container" style="margin-top:20px;">
		<div class="row">
			<div class="col-md-4" style="border: solid 1px #eee;">
				<div ng-repeat="guest in guests">
					{{guest.Name}}
				</div>
			</div>
			<div class="col-md-8" style="border: solid 1px #eee;">
				&nbsp;
			</div>
		</div>
	</div>
</body>

Previewボタンをクリックして画面を確認してみましょう。

2 column

ちゃんと4:8で分割されていますね。

マークアップの構造をみていきましょう。
divで階層構造が追加されています。クラスでみると、container > row > col-md-* という具合に3層あります。

まずcontainerはグリッドシステムを利用する際、最上位に必要なエレメントとなります。

次にrowですが、これは文字通り行を意味しています。この行の中に最大12列を作成することができます。

最後にcol-md-*ですが、これは列をあらわしており、最後に幅(列数)を意味する12以下の数字を指定します。今回はcol-md-4とcol-md-8と指定していますので、画面が4:8で分割されます。また真ん中のmdの部分は想定するデバイス(画面サイズ)を意味しておりxs / sm / md / lgの中から選択します。

例えばmdだと、指定通り4:8で画面が分割されるためには最低992pxの画面幅が必要になり、それ以下の画面サイズでは画面は分割されず縦に並びかえられます。これはレスポンシブデザインを意識した仕様です。

また、col-md-*の要素にborderスタイルを追加していますが、これは画面の構成を確認するために一時的に指定しています。

次にList Groupを適用してゲスト一覧にスタイルを適用していきます。

<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-repeat="guest in guests">{{guest.Name}}</a>
					</div>
				</div>
			</div>
			<div class="col-md-8">
				&nbsp;
			</div>
		</div>
	</div>
</body>

Previewボタンをクリックして画面を確認してみます。

list group applied

こちらは特に解説のしようがないのですが、Bootstrapの記法でこう記述するとリストがこのように表示される、ということですね。より詳しくはBootstrapドキュメントの該当チャプターを参照ください。
http://getbootstrap.com/components/#list-group

今回はここまで。現時点での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>
    <c:RemoteTK />
    <script>
    var ngbootcamp = angular.module('ngbootcamp', []);
    
    ngbootcamp.controller('guestCtl', function($scope){
        var force = new remotetk.Client();
        var soql = "select Id, Name from guest__c";
        force.query(
            soql,
            function(result){
                $scope.guests = result.records;
                $scope.$apply();
            },
                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-repeat="guest in guests">{{guest.Name}}</a>
                    </div>
                </div>
            </div>
            <div class="col-md-8">
                &nbsp;
            </div>
        </div>
    </div>
</body>
</html>
 
</apex:page>

以降のHTMLは常にBootstrapをベースに記述していきます。

次回はゲストを選択するとその詳細情報が右側のエリアに表示されるというインタラクティブな仕組みを構築していきます。

 

関連情報

without comments

Written by 中嶋 一樹

2月 8th, 2014 at 4:17 pm