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を用いたレスポンシブデザインでモバイル対応