Archive for 2月, 2014
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> 新規</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> ゲスト一覧へ </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> 新規</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> ゲスト一覧へ </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()">×</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>
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
Part7の続きです。
今回は新規ゲスト作成フォームを実装していきます。Part6で登場したモーダルダイアログ、そしてPart7で登場したPromise/Deferredパターンを駆使していきます。
まずは今回開発する部分をデモでみてみましょう。
それでは実装へ。
新規ゲストフォームのモーダルダイアログを作成する
まずモーダルダイアログとなるHTMLマークアップを作成しておきましょう。
<!-- Modal for newGuestForm --> <script type="text/ng-template" id="T_newGuestForm"> <div class="modal-header"> <button type="button" class="close" ng-click="$dismiss()">×</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> 新規</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> 新規</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()">×</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>
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
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>
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
Part5の続きです。
今回は更新ボタンをクリックした後に経過を表示するプログレスバーと、重複送信を防止するモーダルウィンドウを合わせて実装していきます。
まずデモで動作を確認してみましょう。
それでは実装へ。
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が提供するカスタムディレクティブ、
<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>
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
AngularJSではじめるHTML5開発 – Part5 データの更新
Part4の続きです。
今回はデータの更新をおこないます。
まずは今回開発する部分をデモでみてみましょう。
それでは実装へ。
レコードを更新する関数を作成する
レコードを取得する関数と同様に更新する関数を作成します。
RemoteTKにはupdate()というレコード更新のためのメソッドが用意されていますのでこれを利用します。
$scope.updateGuest = function(){ var guest = angular.copy($scope.guest); delete guest.attributes; $scope.force.update( "guest__c", $scope.guest.Id, guest, function(result){ alert("レコードが更新されました。"); }, function(result){ console.log(result); } ); }
第一引数にオブジェクト名のguest__c、第二引数にレコードIDの$scope.guest.Id、第三引数に更新するデータをオブジェクトで指定します。
第三引数のデータは、本当なら$scope.guestをそのまま渡してあげたいところですが、実はこのオブジェクトには元々のguest__c オブジェクトには存在しないattributesというプロパティが追加されており、これをそのままRemoteTKのupdateに渡すと「そんな項目ないよ」というエラーになってしまいます。したがって、事前にこのプロパティを消す必要があります。
ただし、一応元のオブジェクトはそのまま維持しておくために一旦オブジェクトを更新専用オブジェクトにコピーしてから削除しています。
var guest = angular.copy($scope.guest); delete guest.attributes;
angular.copy()はオブジェクトまたは配列のDeep Copyをおこなうメソッドです。これでコピーして更新専用オブジェクトguestを作成し、このguestからattributesを削除しています。
これでレコードを更新する関数ができました。
更新ボタンを設置する
更新を実行するためのボタンを設置します。
<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>
クラスのbtnおよびbtn-successはBootstrapのスタイルを適用させているだけです。
重要なのはng-clickでクリック時に先ほど作成したupdateGuest()を実行しているところですね。
さて、実際にプレビューしてみましょう。任意のレコードをクリックし、Emailを入力して「更新」をクリックしてください。
「レコードを更新されました。」というアラートが表示されれば成功です。
ただし、このデータ更新の仕組み、まだまだ改善の余地があります。
まず、アラートが表示されるまでの間、処理が実行されているのかどうか、またその経過がわかりません。
また、Emailではなくゲスト名を更新してみるとわかりますが、更新が完了したにも関わらずサイドバーのゲスト名は古いままの表示になっています。
ということで次回はこのAjax処理中(更新処理中)の経過をユーザーに表示しつつ、重複送信等の誤操作を防止する仕組みを実装したいと思います。
例によって最後に現時点での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){ $scope.updateGuest = function(){ var guest = angular.copy($scope.guest); delete guest.attributes; $scope.force.update( "guest__c", $scope.guest.Id, guest, function(result){ alert("レコードが更新されました。"); }, 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> </body> </html> </apex:page>
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
Part3の続きです。
今回はレコードをクリックするとデータベースから詳細情報を取得し、その詳細情報で画面を更新するという仕組みを実装していきます。
まず今回開発する部分をデモでみておきましょう。
それでは実装へ。
ゲストオブジェクトに項目(フィールド)を追加する
データベースから詳細情報を取得する、というほどまだオブジェクトには項目がありません。アプケーションで利用できそうなのは自動的に追加された「ゲスト名」くらいです。これでは寂しいのでEmailを保持する項目くらい追加しておきましょう。
スキーマビルダーを起動し、要素タブから「メール」を選択して右側のゲストオブジェクトの上にドラッグ&ドロップします。
下記のように入力して保存をクリックします。
- 項目の表示ラベル => Email
- 項目名 => email
これで項目が追加されました。
レコード詳細情報を取得する関数を作成する
Part2ではRemoteTKのquery()メソッドを用いて、SOQL構文の実行によってデータベースからゲストオブジェクト中のレコード一覧を取得しました。
このquery()を用いて単一のレコード情報を取得することも当然可能ですが、単一のレコードを取得する場合はより簡単なretrieve()というメソッドが用意されているのでこちらを利用することにしましょう。
また、レコード一覧を取得する部分はindexファイルを読み込むやいなや実行されていましたが、今回はクリックのタイミングで実行する必要があるため、外部から呼び出し可能な関数としてまとめておきます。
ngbootcamp.controller('guestCtl', function($scope){ $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(); }
ゲストレコードの詳細を取得する関数、$scope.getGuest()が追加されています。また、先に作成していたゲストレコード一覧を取得するコードも$scope.getGuests()として関数化しています。また、RemoteTKのインスタンスも$scope.forceにセットして各関数から自由に参照できるようにしています。
$scope.getGuest()の内部ではRemoteTKのメソッドであるretrieve()が実行されています。
このメソッドは、第一引数にオブジェクト名、第二引数にレコードID、そして第三引数にカンマ区切りの項目リストをとります。今回はオブジェクト名がguest__c、レコードIDはrecordId、項目リストはIdとName、そしてemail__cということになります。
第四引数、第五引数はquery()と同様、成功時と失敗時のコールバック関数です。
成功時のコールバックでは$scope.guestに取得したデータをセットし、$scope.$apply()でデータモデルに反映させています。
これでレコード詳細を取得する関数は出来上がりました。
クリックで関数を実行する仕組み
とっても簡単です。各レコードを表示させている<a>タグにng-clickという属性を追加してあげます。これはJavascriptのonclickと同様の機能を持ちますが、AngularJSで作成した関数を呼べる、というところがポイントです。
<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>
getGuest()の引数にちゃっかりguest.Idが指定されています。これによってどのレコードの詳細を取得すべきか判断しているわけです。
これでレコードがクリックされると先ほど作成した$scope.getGuest()が呼ばれ、データベースからレコードの詳細を取得するようになりました。
画面を更新する
この部分が特にAngularJSによってスマートに実装できる部分です。
データベースから取得されたレコード詳細情報は$scope.guestに格納されています。HTML側ではこの変数を参照しておけば値が入ってきた時点で表示されるようになります。
<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> </form> </div>
<h1>タグ内では{{guest.Name}}を用いてダブル波括弧で変数を参照しています。
インプットフィールドではng-modelを用いてAngularJSの変数にバインドさせています。
実際の動作をプレビューで確認してみましょう。
レコードをクリックすると右列に詳細が表示されるようになっていますね。
ただし、Emailフィールドは空のままです。そりゃそうだ。まだ値自体がデータベースに保存されていませんから。
ということで次回はデータを更新する処理をみていきたいと思います。
例によって現時点での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){ $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> </form> </div> </div> </div> </body> </html> </apex:page>
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
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;"> </div> </div> </div> </body>
Previewボタンをクリックして画面を確認してみましょう。
ちゃんと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"> </div> </div> </div> </body>
Previewボタンをクリックして画面を確認してみます。
こちらは特に解説のしようがないのですが、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"> </div> </div> </div> </body> </html> </apex:page>
以降のHTMLは常にBootstrapをベースに記述していきます。
次回はゲストを選択するとその詳細情報が右側のエリアに表示されるというインタラクティブな仕組みを構築していきます。
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
Part1の続きです。
今回はForce.comのデータベースからデータを取得します。
まず今回開発する部分をデモでみてみましょう。
なんだか意味がわからないですね。でも大丈夫です。
それでは実装へ。
オブジェクト(テーブル)を作成する
Force.comではテーブルのことを「オブジェクト」と呼んでいます。オブジェクト作成はスキーマビルダーというGUIツールで猫でもおこなうことができるようになっています。
Developer Editionで左のサイドバーから「スキーマビルダー」をクリックします。
スキーマビルダーが起動しました。
次に左のサイドバーから「要素」タブを選択し、「オブジェクト」を右のキャンバスエリアにドラッグ&ドロップします。
新しいオブジェクトの情報を入力するダイアログが表示されます。今回は汎用的な人の情報を入力するための「ゲスト」というオブジェクトを作成してみます。
下記の通りに入力して保存をクリックします。その他の項目はデフォルトで構いません。
- 表示ラベル => ゲスト *UIに表示されるオブジェクトのラベル
- オブジェクト名 => guest *プログラムでアクセスするときに指定するオブジェクト名
- レコード名 => ゲスト名 *最初に自動作成される項目(フィールド)のラベル
- データ型 => テキスト *最初に自動作成される項目のデータ型
Force.comでオブジェクトや項目を作成する際、ほとんどのケースで「ラベル」と「名」をセットで指定します。ラベルはUIに表示される文字列で、名はプログラムからアクセスする際の正式名称になります。
これでオブジェクトが作成されました。
ちなみに「ゲスト名」に加えて「最終更新者」「作成者」「所有者」という項目が自動で作成されていることがわかりますが、これらはシステム側でメンテされますので現時点では意識する必要はありません。
さて、オブジェクトはこれで作成は完了です。
RemoteTKをインストールする
先ほど作成したオブジェクトにJavascriptから簡単にアクセスするためのツールキット:RemoteTKがあります。これをインストールしましょう。下記のURLをクリックすると、Develoepr EditionにRemoteTKをインストールすることができます。
https://login.salesforce.com/packaging/installPackage.apexp?p0=04td0000000IgBb
*RemoteTKはForce.com Javascript REST Toolkitに含まれるライブラリです。
「次へ」をクリックします。
下記の通りインストールを進めます。
- ステップ1.パッケージAPIアクセスの承認 => そのまま「次へ」をクリック。
- ステップ2.セキュリティレベルの選択 => そのまま「次へ」をクリック。
- ステップ3.パッケージのインストール => 「インストール」をクリック。
インストールが下図のように完了します。
AngularJSからデータベースに問い合わせをおこなう
今のところゲストオブジェクトにはデータが全くはいっていないので2つほどレコードを作成しておきましょう。
開発者コンソールを開きます。
メニューからDebug > Open Execute Anonymous Windowをクリックします。
このExecute Anonymous Windowでは任意のApexコードを実行することができます。ApexコードとはForce.com独自のプログラム言語ですが、Javaに酷似した記法を採用していますのでJavaが読み書きできる人はあまり違和感ないと思います。
いずれにせよ、今は深く知る必要はありません。下記のコードをコピペしてExecuteボタンをクリックしてください。
insert new guest__c(name = 'Naoki Hanzawa'); insert new guest__c(name = 'Shunichi Kurosaki');
よっしゃー、データを取得するJavascriptを書いていきましょう。前回作成したVisualforceページ、indexを編集します。
開発者コンソールのメニューからFile > Openをクリックします。
Pages > indexを選択しOpenボタンをクリックします。
まず、RemoteTKを読み込みます。
<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" > <html ng-app="ngbootcamp"> <head> <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){ $scope.guestName = 'Kazuki Nakajima'; }); </script> </head> <body ng-controller="guestCtl"> <input type="text" ng-model="guestName" /> <h1>{{guestName}}</h1> </body> </html> </apex:page>
<c:RemoteTK />はVisualforceのマークアップで、実質的にはツールキットに含まれるJavascriptライブラリがこの場所に挿入されることになります。これによって以後のコードでライブラリが利用できるようになります。
次にJavascriptでデータベースに問い合わせをおこなうコードを追記します。
<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" > <html ng-app="ngbootcamp"> <head> <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"> </body> </html> </apex:page>
ひとつずつみていきましょう。
var force = new remotetk.Client();
RemoteTKライブラリのインスタンスを初期化してforceにセットしています。
var soql = "select Id, Name from guest__c";
これはデータベースに問い合わせするクエリです。Force.comデータベースではSQL構文に類似の「SOQL」という構文を用います。
とはいってもこのような基本的な問い合わせはSQLとまったく同じです。
問い合わせ対象オブジェクトにguest__cを指定しています。Part1でゲストオブジェクトを作成した際、「guest」というオブジェクト名を設定しました。気になるのは語尾についている「__c」ですよね。
これは開発者が作ったオブジェクトに自動的に付与される接尾辞です。Force.comにはシステムに元々存在するオブジェクトがあり、これらは標準オブジェクトと呼ばれます。逆に、開発者が作成したオブジェクトはカスタムオブジェクトと呼ばれ、オブジェクト名の語尾に「__c」が自動的に追加されます。
なお、これはオブジェクトに限らず、項目(フィールド)についても同様です。今現在ゲストオブジェクトに存在している項目はすべてシステムが自動生成したものなので「__c」は付いていませんが、これ以後に開発者が項目を追加したらその項目には「__c」が付加されます。*ちなみに項目に指定しているIdはすべてのレコードに対して自動的に生成される一意な識別子です。
force.query( soql, function(result){ $scope.guests = result.records; $scope.$apply(); }, function(result){ console.log(result); } );
RemoteTKライブラリに備わっているメソッド、query()を実行しています。これは任意のSOQLクエリを発行するためのメソッドです。
第一引数はクエリです。先にセットしておいたsoqlを渡しています。
第二引数はquery()が成功したときのコールバック関数です。
resultにはクエリの結果はセットされて返ってきます。この結果の詳細はconsole.log()などでみてみるとわかりますが、result.recordsにレコードのリストがセットされています。それを$scope.guestsに保存し、AngularJSフレームワークで利用できるようにしています。
次に実行されている$scope.$apply()は、Ajaxリクエスト等で取得したデータをAngularJSのデータモデルに反映するために必要な処理です。少し難しいですね。
現時点では、RemoteTKでデータを取得したら$scope.$apply()を呼ぶ、と覚えておけばよいと思います。
第三引数はquery()が失敗したときのコールバック関数です。
エラー情報がresultに入っているので単純にそれをconsole.logで吐き出しています。
最後に、取得したデータをHTML上に表示します。
<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" > <html ng-app="ngbootcamp"> <head> <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 ng-repeat="guest in guests"> {{guest.Name}} </div> </body> </html> </apex:page>
ng-repeat、きましたね。
これはng-*という属性の中でも特に利用頻度が高いものの一つになると思います。
guestsはデータベースから取得したゲストレコードのリストを参照しています。それがひとつずつguestに代入されてループ処理されます。
この内部ではguestが参照できるので、guest.Nameといった形でオブジェクトの項目を参照しているわけです。
右上のプレビューボタンをクリックして動作を確認してみましょう。
先に作成したレコード2つが表示されていればOKです。
まだまだ原始的ですが、Javascriptからデータベースに問い合わせをおこない、結果をAngularJSで表示させるという基本的な処理をマスターすることができました。
次回は多くのサイトで採用されているUIフレームワークであるBootstrapを適用してこの退屈なデモアプリをよりHTML5っぽくしていきたいと思います。
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応
AngularJSではじめるHTML5開発 – Part1 Getting Started
これから数回に分けてAngularJSでHTML5アプリを開発していきたいと思います。
開発環境は最高のHTML5プラットフォームであるForce.comですが、Force.comおよびAngularJSをまったく使ったことがない開発者の方々でも試せることを前提にした内容です。
ただし一点、一応IE以外で検証ください。
今回は最初の最初、ゼロからはじめてForce.com上でAngularJSが動くまでを解説していきます。
まずこのPart1で開発する部分をデモでみてみましょう。
それでは実装へ。
開発組織の入手
Force.comの開発環境は「Developer Edition」と呼ばれ、誰でも、いくつでも、いつまででも無料です。さらに本番環境で利用できるほぼすべての機能が利用できます。なんということでしょう。
ということで、まずは下記のサイトからこのDeveloper Editionを入手します。
http://events.developerforce.com/signup?d=70130000000EjHb
サインアップが完了してしばらくすると、メールでログイン情報が届きます。このメールにしたがってログインすると下記のようなサイトにアクセスできます。
Force.comの開発環境は「全部入り」であるため、CRMの機能もはいってしまっています。なのでいきなりごちゃごちゃしていますが、すべて無視してください。今回は他の機能には一切触れずに純粋にプラットフォームとして利用していきます。
HTMLファイルの作成
Force.comではVisualforceページというファイルにHTMLを書いていきます。
Visualforceページは本来Force.com独自のマークアップ言語ですが心配ありません。なぜならVisualforceページは99%、HTML5だけで記述できるからです。独自マークアップを利用せずにHTML、CSS、JavascriptといったWeb開発者の必須科目で開発することができます。
では早速新しいVisualforceページを作成してみます。
アカウント名のプルダウンメニューから「開発者コンソール」を選択します。
このコンソールではコーディング、およびデバッグをおこなうことができます。まずは上部のメニューから File > New > Visualforce Pageを選択します。
ファイル名を入力するフォームが現れますので「index」と入力してOKをクリックします。
*Apex Pageというのがやや謎ですが、Visualforce Pageのことです。
*ファイル名はindexでなくても何でもOKです。
新しいVisualforce Pageが作成され、下記のように謎のタグが入っています。
<apex:page> </apex:page>
このタグはVisualforce独自のマークアップであり、かつ、唯一必須のVisualforceタグです。
Visualforceページでは、ページの最上部に<apex:page>そして最下部に</apex:page>が必ず必要です。
すべてのHTMLコードはこの内側に記載していきます。
早速HTMLを記述していきたいところですが、その前に一つだけ。<apex:page>タグを下記のように修正してあげてください。
<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" > </apex:page>
これは「まっさらなページにHTML5コードを書きますよ」という宣言です。
Visualforceについて知っておかなければいけないことは以上です。
編集したコードはキーボードでCtrl + s または Command + sといったショートカットキーで保存できます。
保存ができたら次に進みましょう。
AngularJSをインポート
AngularJSのセットアップは一つのJavascriptファイルを読み込むだけです。googleやcdnjsのようなCDNサービスから読み込むことができます。
<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" > <html> <head> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.10/angular.min.js"></script> </head> </html> </apex:page>
これで完了です。
超重要機能 データバインディングを試してみる
さて、早速Angularしてみましょう。
AngularJSといえばJavascript内のデータモデルが動的にHTMLに反映される、というのが一つの醍醐味です。この動作を確認するためにシンプルなHTMLとJavascriptのコードを記述してみます。
<apex:page showHeader="false" standardStyleSheets="false" applyBodyTag="false" applyHtmlTag="false" docType="html-5.0" > <html ng-app="ngbootcamp"> <head> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.10/angular.min.js"></script> <script> var ngbootcamp = angular.module('ngbootcamp', []); ngbootcamp.controller('guestCtl', function($scope){ $scope.guestName = 'Kazuki Nakajima'; }); </script> </head> <body ng-controller="guestCtl"> <input type="text" ng-model="guestName" /> <h1>{{guestName}}</h1> </body> </html> </apex:page>
まずはこのコードを保存し、コンソール右上のPreviewボタンをクリックして動作をみてみましょう。
下図のようにテキストフィールドと、h1タグが効いた文字列が表示されているはずです。
次にテキストフィールドの値を変更してみてください。そうするとh1タグの文字列も連動して変更されるはずです。
ここでわかるのは下記2点です。
- Javascriptで$scope.guestNameに代入した初期値(Kazuki Nakajima)がHTMLにも反映されているようだ。
- テキストフィールドの値と<h1>タグの値はつながっているようだ。
AngularJSではこの仕組みを「データバインディング」と呼んでおり、おそらくは最も重要な機能だと思います。
仕組みをよくみてましょう。
<html ng-app="ngbootcamp">
これはこのページでAngularJSを有効化するための指定です。
ngbootcampというのは利用するAngularJSのモジュールを指定しています。モジュールはこの後にJavascriptで作成します。
var ngbootcamp = angular.module('ngbootcamp', []); ngbootcamp.controller('guestCtl', function($scope){ $scope.guestName = 'Kazuki Nakajima'; });
angular.module()というメソッドでAngularJSのモジュールを”ngbootcamp”という名前で作成しています。(このモジュール名は任意のものでかまいません)
このメソッドはモジュールのインスタンスを返します。つまりvar ngbootcampがそのインスタンスとなっています。
ngbootcamp.controller()ではコントローラを作成しています。
このメソッドは第一引数にコントローラ名、第二引数に無名関数でコントローラのプロパティやメソッドを定義します。
このコントローラ内で定義した$scope.guestNameのようなプロパティはHTMLから参照できるようになります。$scopeというのはこのコントローラの空間を示しているオブジェクトです。ちょっと難しいですが$scope.を先頭につけた変数はAngularがしっかり管理してくれて、JavascriptでもHTMLでもアクセスできるようになる、くらいの理解でまずは良いと思います。
<body ng-controller="guestCtl">
普段は見かけない属性「ng-controller」なるものがあります。AngularJSではng-*というような形式の追加属性がたくさん用意されており、これによってHTMLの機能を拡張しています。このng-controllerは、Javascript内で定義されているAngularJSのコントローラーを指定することで、この要素(body)の配下ではguestCtlというコントローラーが利用できる、ということになります。
<input type="text" ng-model="guestName" />
また出ました。今度はng-model。これは極めて重要な属性で、コントローラの変数と、このテキストフィールドの値をくっつけます。つまり、$scope.guestNameとこのテキストフィールドの値は常に同期することになります。なのでJavascriptで$scope.guestNameに代入したKazuki Nakajimaという値が最初にテキストフィールドに入っていたわけです。
<h1>{{guestName}}</h1>
こんどはダブル波括弧が登場です。このダブル波括弧で変数名を指定することにより、コントローラーで定義した$scope.guestNameにアクセスすることができるわけです。
そして、コントローラー内の変数、テキストフィールド、h1タグの値はすべての同じオブジェクトを参照しているため、常に値が同期しています。
鋭いかたは、「ng-modelの方は波括弧をつけていないじゃないか」、と思われたかもしれません。
その通りなのですが、ng-modelはAngular独自の属性であり、その値はすでにAngularの縄張りにはいっています。なので波括弧をつけずともAngular世界の変数であることが認識できるので波括弧はつけません。一方、h1タグの方は波括弧をつけないとそのままguestNameと出力されるますので明示的に波括弧でAngular世界のオブジェクトであることを教えてあげるわけです。
というわけで、Force.com上でAngularJSが動くようになりました。
冒頭にあえてAngularJSとは?のようなイントロをいれませんでしが、それはここまで試してみるとどう便利なのかが肌でわかってくると思ったからです。
お察しの通り、AngularJSを使うと動的にページレイアウトを変更したり、フォームの中身を簡単に取得してAPIコールをおこなったりすることが極めて容易に実現できます。そのコードも非常にクリーンです。
次回はデータベースとのやり取りを含めた実装をみていきたいと思います。
関連情報
- AngularJSではじめるHTML5開発 – Part1 Getting Started
- AngularJSではじめるHTML5開発 – Part2 データベースにアクセスする
- AngularJSではじめるHTML5開発 – Part3 Bootstrapの適用
- AngularJSではじめるHTML5開発 – Part4 動的なデータベースアクセスと画面の更新
- AngularJSではじめるHTML5開発 – Part5 データの更新
- AngularJSではじめるHTML5開発 – Part6 UI Bootstrapを用いたプログレスバーとモーダルダイアログ
- AngularJSではじめるHTML5開発 – Part7 Promise/Deferredを用いたデータ更新後の画面リフレッシュ
- AngularJSではじめるHTML5開発 – Part8 モーダルダイアログによる新規レコード作成フォーム
- AngularJSではじめるHTML5開発 – Part9 enquire.jsを用いたレスポンシブデザインでモバイル対応