アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Monacaを使ってKintoneを手軽にアプリ化しましょう

業務で使うちょっとしたWebアプリケーションを手軽に作れるのがサイボウズの提供するKintoneです。日報や議事録管理、タイムカード、アンケートなど様々なWebアプリケーションが用意されており、ExcelCSVファイルを使って自分たちのワークフローに合わせたWebアプリケーションを作ることもできます。



KintoneはWebアプリケーションなので、Webブラウザを使って操作します。しかし、Monacaを使えばKintoneの提供するAPIを使って、手軽にアプリ化することが可能です。ぜひ活用してください。



必要なもの





案件管理をアプリ化する



今回は営業の案件状況を管理する案件管理をアプリ化してみたいと思います。まずは最小限のテンプレートでプロジェクトを作成します。





最小限のテンプレート



次に設定メニューのJS/CSSコンポーネントの追加と削除を選択します。





JS/CSSコンポーネントの追加と削除



そしてOnsen UI追加してください。バージョンは最新版とします。





Onsen UIを追加



HTMLの修正



index.htmlを次のように変更します。AngularJS用にmyAppを定義し、一緒にOnsen UIも読み込むようにします。





<!DOCTYPE HTML>
<html ng-app="myApp" lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <script src="components/loader.js"></script>
    <link rel="stylesheet" href="components/loader.css">
    <link rel="stylesheet" href="css/style.css">
    <script>
        var myApp = angular.module('myApp', ['onsen']);
    </script>
</head>
<body style="height:100%">
    <ons-navigator var="myNavigator" page="login.html"> 
    </ons-navigator> 
</body>
</html>


次にindex.htmlと同じ階層にlogin.htmlを作成します。これは初期のログイン画面を表示するものです。内容は次のようになります。





<ons-page ng-controller="LoginController" style="background:#cedfe5;">
  <!-- 読み込み中表示のモーダル用 -->
  <ons-modal var="modal">
    <ons-icon icon="ion-load-c" spin="true"></ons-icon>
    <br><br>
    Please wait.
  </ons-modal>
  
  <!-- ログインフォーム -->
  <div id="loginContainer">
    <div id="loginContents">
      <h4 id="subTitle"  >kintone連携</h3>
      <h3 id="title">商談記録入力アプリ</h2>    
      <ons-row id="url" align="center">
        <ons-col align="right" width="20%" class="fontSmaller">https://</ons-col>
        <ons-col width="50%"><input placeholder="Domain" type="text" id="test2" ng-model="domain" class="loginItems"></ons-col>
        <ons-col align="left"  width="30%" class="fontSmaller">.cybozu.com</ons-col>
        
      </ons-row>
      <ons-row>
        <ons-col width="20%"></ons-col><ons-col width="50%"><input placeholder="Login Name" type="text" ng-model="loginName" class="loginItems"></ons-col>
        <ons-col></ons-col>
        
      </ons-row>
      <ons-row>
        <ons-col width="20%"></ons-col><ons-col width="50%"><input placeholder="Password" type="password" ng-model="loginPass" class="loginItems"></ons-col>
        <ons-col></ons-col>
      </ons-row>
      <ons-row id="saveInfo"> 
        <ons-col><input type="checkbox" ng-model="remember">Remember me</ons-col>
      </ons-row>
      <ons-button ng-click="showList()" modifier="quiet" id="loginButton">Login</ons-button>  
    </div>
  </div>
</ons-page>


ここで大事なのはKintoneにアクセスする際には、



  • domain:ドメイン
  • loginName:ログインID
  • LoginPass:パスワード


という3つの要素が必要になるということです。ここまでの時点でアプリをMonacaデバッガーで開くと次のように表示されます。





ログイン画面



ログイン処理を作る



ではログイン処理を作っていきます。まずKintone APIを使う際にはBase64ライブラリが必要です。そこでJavaScriptによるBASE64変換−ちゃよて・ちゃよてよりbase64.jsをダウンロードし、Monacaプロジェクトのjsフォルダ(最初はありませんので新規作成してください)にアップロードします。また、jsフォルダ内にkintone.jsを作成してください。



上記二つのファイルはindex.html内で読み込んでください。





<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <script src="components/loader.js"></script>
    <link rel="stylesheet" href="components/loader.css">
    <link rel="stylesheet" href="css/style.css">
    <script>
        var myApp = angular.module('myApp', ['onsen']);
    </script>
	<!-- 追加ここから -->
    <script src="js/base64.js"></script> 
    <script src="js/kintone.js"></script>
	<!-- 追加ここまで -->
</head>


上記コードのLoginボタンをタップした際にはshowListを実行しています。これは後ほどログイン成功時に案件リストを取得するためですが、まずはログイン処理までを作ってみます。kintone.jsを編集します。





// 認証情報は他でも使い回すのでグローバルにしておきます。
var authorization; // 認証用

myApp.controller("LoginController", function ($scope, $http, ItemService) {

  // ログイン処理を行います
  $scope.showList = function () {
    ItemService.domain = $scope.domain;
		
    console.log("ログイン処理開始");
	
    // 入力を元にログインに必要な情報をセット
    $scope.url = "https://"+$scope.domain+".cybozu.com/k/v1/records.json";
    authorization = base64encode($scope.loginName+":"+$scope.loginPass);
  
    // kintoneAPIdocumentに従って送信するデータを作成
    $scope.req = {
      url: $scope.url,
      method: "GET",
      headers: {
        "X-Cybozu-Authorization": authorization,
        "Content-Type": "application/json"
      },
      params: {
        "app": 13
      }
    };
  
    // httpリクエストを送信
    $http($scope.req)
      .success(function success(data, status, headers, config) {
        // ログイン成功時の処理
        console.log("ログイン成功しました");
        if ($scope.remember == true) {
          // ログイン情報を記録する場合
		  // localStorageに記録しておきます。
          localStorage.setItem("LoginName", $scope.loginName);
          localStorage.setItem("Password",$scope.loginPass);
          localStorage.setItem("Domain",$scope.domain);
        } else if (localStorage.getItem("LoginName")) {
          // ログイン情報を記録しない場合
          localStorage.removeItem("LoginName");
          localStorage.removeItem("Password");
          localStorage.removeItem("Domain");
        }
      })
      .error(function error(data, status, headers, config) {
        alert("ログイン名またはパスワードが違います。");  
      });
  };
  
  // 端末にログイン情報が残っていた場合はそれを元にデータの取得を行う
  if (localStorage.getItem("LoginName")) {
    $scope.loginName = localStorage.getItem("LoginName");
    $scope.loginPass = localStorage.getItem("Password");
    $scope.domain = localStorage.getItem("Domain");
    $scope.remember = true;
    $scope.showList();
    //ons-modalを表示
    $(document.body).on("pageinit", function () {
      modal.show();
      setTimeout("modal.hide()", 1000);
    });
  }
});

myApp.service("ItemService", function () {

});


そうすると、コンソールのログに「ログイン処理開始」「ログイン成功しました」というメッセージが出るかと思います。KintoneのサーバがCORSに対応していないのでデスクトップブラウザのプレビューからではエラーが出るのでご注意ください。





コンソールで確認



案件情報の取得



ログインが成功しましたので、次に案件情報の取得を行ってみます。ログイン処理の最後に以下のコードを追加します。





if ($scope.remember == true) {
  // ログイン情報を記録する場合
		  // localStorageに記録しておきます。
  localStorage.setItem("LoginName", $scope.loginName);
  localStorage.setItem("Password",$scope.loginPass);
  localStorage.setItem("Domain",$scope.domain);
} else if (localStorage.getItem("LoginName")) {
  // ログイン情報を記録しない場合
  localStorage.removeItem("LoginName");
  localStorage.removeItem("Password");
  localStorage.removeItem("Domain");
}

// 追加ここから
//ログインに成功したら一覧画面へ
myNavigator.pushPage("list.html",{animation:"none"});
// 追加ここまで


これでlist.htmlを読み込むようになりますので、index.htmlと同じ階層にlist.htmlを作成します。



list.htmlの内容



list.htmlの内容は次のようになります。





<ons-page ng-controller="MainController" style="background:#cedfe5">
  <ons-toolbar>
    <div class="left" ng-click="signout()" id="signoutButton">
      <ons-toolbar-button><ons-icon icon="fa-sign-out"></ons-icon>Signout</ons-toolbar-button>
    </div>
    <div class="center">案件一覧</div>
  </ons-toolbar>

  <div id="listContainer">
      <div ng-repeat="item in items track by $index"  ng-click="create(item)" id="listContents">
          <div>{{item['文字列__1行_']['value']}}</div>
          <div id="listHorizentalLine" style=""></div>
          <div id="listSubInfo" style="">{{item['文字列__1行__0']['value']}} </div>
      </div>
  </div>
</ons-page>


kintone.jsの修正



list.htmlにおいて案件の一覧はitemsに入ってくる指定になっています。そこで js/kintone.js を修正します。



まず $scope.items を初期化します。





$scope.showList = function () {
  ItemService.domain = $scope.domain;
  $scope.items = []; // 追加
    :


次にログイン時に使ったメソッドはそのまま案件の一覧が返ってきますので、それを保存するようにします。





.success(function success(data, status, headers, config) {
  // 追加ここから
  //返ってきた配列データをItemService.items[]に格納
  for (var i=0; i<data.records.length; i++) {
    $scope.items.push(data.records[i]);
  }
  ItemService.items = $scope.items;
  // 追加ここまで


そして最後に MainController を定義します。





myApp.controller("MainController", function ($scope, $http, ItemService) {
  $scope.items = ItemService.items;
});


これで処理が完成です。



案件の一覧を取得してみる



ここまでできていると、ログイン成功すると案件の一覧が取れるようになります。





案件の一覧



応用編



さらに見た目を整えたり、案件一覧の中からタップすることで活動報告を行えるようにすることで、本当のアプリらしくなっていきます。





案件管理デモ






Kintoneの良いところはデスクトップ用のWebアプリケーションはごくごく簡単に、さらにWeb APIを通じて操作もできることです。Monacaを使えばiOS/Androidの両方に対応したハイブリッドアプリも簡単に開発できます。業務向けスマートフォンアプリ開発MonacaとKintoneをぜひ活用してください。

AngularJSとOnsen UI で作るGoogle Mapsのサンプルアプリ

今回の記事は、Onsen UI blogで2月に公開した"Creating Google Maps Sample App with AngularJS and Onsen UI"の翻訳記事です。



以下のツールを使用して、このサンプルを構築します。

Onsen UI ( HTML5 を使用したハイブリッドアプリを作成するためのフレームワーク )
AngularJS ( Google 社が開発した JavaScript のフレームワーク )
Google Maps JavaScript API v3

ここでは、Monacaを使用して、アプリを開発します。Monaca は、クロウド型のアプリ開発環境です。このツールを使用すれば、マルチプラットフォーム ( iOS/Android/Windows8 ) に対応するハイブリッドアプリを開発できます。Monaca には、Onsen UI のフルサポート、テンプレートの提供など、開発を容易にしてくれる作り込みが多数してあります。これらの点が、今回、Monaca を使用して、開発を行う理由です。

このサンプルアプリに実装している機能は、上記ツールの解説に必要となる、最低限に抑えてあります。また、ここでは、複雑・大規模アプリの構築ではなく、拡張可能な、ひな形となるアプリの構築に焦点を当てています。よって、アプリ内の機能のいくつかは、表示されているだけで、機能しません。開発者側で自由に書き換えれるように、枠組みだけ、提供しています。



実装している機能を、以下に記します。


  • マーカーの表示 ( 画面上で長押し )

  • マーカーの削除 ( 1 件、画面上でタップ )

  • マーカーの削除 ( 全件、<ons-button> を使用)

  • マーカー間の距離 ( 2 点間の距離、総距離 ) の計算 ( <ons-button> を使用 )

  • メニューの表示 ( <ons-sliding-menu> を使用 )



<ons-sliding-menu> を使用して、メインのインターフェイス ( ページ選択の画面 ) を配置し、その上に、map.html と settings.html へのリンクを置いています。map.html には、map 関連の要素と Onsen UI の要素 ( <ons-button> など ) を記述しています。スライティングメニュー上から、map 画面または settings 画面へ遷移できます。

上のサンプルアプリは、実際に操作できます。または、こちらのリンクからブラウザー表示も行えます。GitHub上にも、コードを置いていますので、開発者の環境で検証もできます。

また、上述の HTML コンテンツの他にも、controller.js と style.css の 2 つの重要な部品から、このアプリは構成されています。

コントローラー

コントローラーには、アプリのロジックを記述しています。このアプリには、2 つのコントローラーを使用しています。1 つは、ons-sliding-menu の制御用で、もう 1 つは、地図の制御用です。どちらのコントローラーも、controller.js 内に記述されています。

SlidingMenuController

ons-sliding-menu 要素と Google Maps 関連の要素を、同時に使用する際の問題点として、スクロール処理のイベントに、どちらも影響されることが挙げられます ( ここでは、水平方向のスワイプ )。よって、スライディングメニューのスワイプ時の挙動を、コントローラー側で制御する必要があります。
ここでは、この問題を解消するために、 'postopen' または 'postclose' イベントの制御を行う要素に、リスナーを登録します。スライディングメニューのスワイプは、デフォルトでは、無効化されていますが、ons-toolbar-button をクリックして、メニューを開くと有効化され、メニューが閉じるまで ( スワイプまたは ons-toolbar-button の再実行 )、そのまま有効化されています。このリスナーは、スライディングメニューを初回に開いた後に、設定されます。

MapController

このコントローラーには、map オブジェクト関連のロジックと Onsen UI 関連のロジック ( Google Maps と Onsen UI との連携部分 ) を記述しています。最初の処理は、maker 用の配列の作成 ( 後から使用 ) と map 要素の作成です。ここでは、地図
の初期化時に、AngularJS の $timeout を使用して、地図の読み込みを、100ms ほど、遅延させます。DOM を読み込む前に、AngularJS のコントローラーの初期化が行われるため、この処理が必要となります。

地図の読み込み後、タップイベント ( マーカーの表示 ) に対して、リスナーを設定します。ここでは、Hammer.JS ライブラリーを使用します。Hammer.JS は、Onsen UI に、初めから組み込まれています。

地図の初期化後、上述した機能が使用できます。上述の機能には、それぞれ、対応するメソッドがあります ( $scope.addOnClick()、$scope.deleteAllMarkers()、$scope.calculateDistance() )。

$scope.addOnClick() は、上述したリスナーと紐付けされています。画面のクリック/タップ位置 ( X値、Y値 ) を取得して、経緯・緯度の座標に変換します。Google Maps では、このような形式でエンコード化が行われているため、この変換処理が必要となります。この処理には、以下のような関数を使用します。



$scope.overlay.getProjection().fromContainerPixelToLatLng(point);


地図を入れる div の左上の地点に、座標の原点があります ( 端末の画面の左上ではありません )。これが、Y 座標の取得時に、(-) 44px 分だけ、差し引く理由です ( 44px は、画面上部に置かれた <ons-toolbar> 要素の高さです )。座標の変換後、地図上に、マーカーを表示できます ( マーカー用の配列を使用 )。

次に、マーカーに対するクリック/タップに備えて、リスナーを設定します。マーカーが押された場合、ons.notification.confirm 要素を使用して、マーカーを削除するか否か、ユーザー側に確認します。次に、ユーザーの選択肢に応じて、ons.notification.alert 要素を表示します。



// controller.js

(function() {
    var app = angular.module('myApp', ['onsen']);

    //Sliding menu controller, swiping management
    app.controller('SlidingMenuController', function($scope){

        $scope.checkSlidingMenuStatus = function(){

            $scope.slidingMenu.on('postclose', function(){
                $scope.slidingMenu.setSwipeable(false);
            });
            $scope.slidingMenu.on('postopen', function(){
                $scope.slidingMenu.setSwipeable(true);
            });
        };

        $scope.checkSlidingMenuStatus();
    });

    //Map controller
    app.controller('MapController', function($scope, $timeout){

        $scope.map;
        $scope.markers = [];
        $scope.markerId = 1;

        //Map initialization  
        $timeout(function(){

            var latlng = new google.maps.LatLng(35.7042995, 139.7597564);
            var myOptions = {
                zoom: 8,
                center: latlng,
                mapTypeId: google.maps.MapTypeId.ROADMAP
            };
            $scope.map = new google.maps.Map(document.getElementById("map_canvas"), myOptions); 
            $scope.overlay = new google.maps.OverlayView();
            $scope.overlay.draw = function() {}; // empty function required
            $scope.overlay.setMap($scope.map);
            $scope.element = document.getElementById('map_canvas');
            $scope.hammertime = Hammer($scope.element).on("hold", function(event) {
                $scope.addOnClick(event);
            });

        },100);

        //Delete all Markers
        $scope.deleteAllMarkers = function(){

            if($scope.markers.length == 0){
                ons.notification.alert({
                    message: 'There are no markers to delete!!!'
                });
                return;
            }

            for (var i = 0; i < $scope.markers.length; i++) {

                //Remove the marker from Map                  
                $scope.markers[i].setMap(null);
            }

            //Remove the marker from array.
            $scope.markers.length = 0;
            $scope.markerId = 0;

            ons.notification.alert({
                message: 'All Markers deleted.'
            });   
        };

        $scope.rad = function(x) {
            return x * Math.PI / 180;
        };

        //Calculate the distance between the Markers
        $scope.calculateDistance = function(){

            if($scope.markers.length < 2){
                ons.notification.alert({
                    message: 'Insert at least 2 markers!!!'
                });
            }
            else{
                var totalDistance = 0;
                var partialDistance = [];
                partialDistance.length = $scope.markers.length - 1;

                for(var i = 0; i < partialDistance.length; i++){
                    var p1 = $scope.markers[i];
                    var p2 = $scope.markers[i+1];

                    var R = 6378137; // Earth’s mean radius in meter
                    var dLat = $scope.rad(p2.position.lat() - p1.position.lat());
                    var dLong = $scope.rad(p2.position.lng() - p1.position.lng());
                    var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
                    Math.cos($scope.rad(p1.position.lat())) * Math.cos($scope.rad(p2.position.lat())) *
                    Math.sin(dLong / 2) * Math.sin(dLong / 2);
                    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
                    totalDistance += R * c / 1000; //distance in Km
                    partialDistance[i] = R * c / 1000;
                }


                ons.notification.confirm({
                    message: 'Do you want to see the partial distances?',
                    callback: function(idx) {

                        ons.notification.alert({
                            message: "The total distance is " + totalDistance.toFixed(1) + " km"
                        });

                        switch(idx) {
                            case 0:

                                break;
                            case 1:
                                for (var i = (partialDistance.length - 1); i >= 0 ; i--) {

                                    ons.notification.alert({
                                        message: "The partial distance from point " + (i+1) + " to point " + (i+2) + " is " + partialDistance[i].toFixed(1) + " km"
                                    });
                                }
                                break;
                        }
                    }
                });
            }
        };

        //Add single Marker
        $scope.addOnClick = function(event) {
            var x = event.gesture.center.pageX;
            var y = event.gesture.center.pageY-44;
            var point = new google.maps.Point(x, y);
            console.log(x + " - " + y);
            var coordinates = $scope.overlay.getProjection().fromContainerPixelToLatLng(point);

            console.log(coordinates.lat + ", " + coordinates.lng);

            var marker = new google.maps.Marker({
                position: coordinates,
                map: $scope.map
            });

            marker.id = $scope.markerId;
            $scope.markerId++;
            $scope.markers.push(marker);

            $timeout(function(){
                //Creation of the listener associated to the Markers click
            google.maps.event.addListener(marker, "click", function (e) {
                ons.notification.confirm({
                    message: 'Do you want to delete the marker?',
                    callback: function(idx) {
                        switch(idx) {
                            case 0:
                                ons.notification.alert({
                                    message: 'You pressed "Cancel".'
                                });
                                break;
                            case 1:
                                for (var i = 0; i < $scope.markers.length; i++) {
                                    if ($scope.markers[i].id == marker.id) {
                                        //Remove the marker from Map                  
                                        $scope.markers[i].setMap(null);

                                        //Remove the marker from array.
                                        $scope.markers.splice(i, 1);
                                    }
                                }
                                ons.notification.alert({
                                    message: 'Marker deleted.'
                                });
                                break;
                        }
                    }
                });
            });
            },1000);


        };
    });
})();


$scope.deleteAllMarkers() で行う処理は、シンプルです。マーカーの配列を確認して、地図上から、すべてのマーカーを削除します。次に、配列から、すべてのデータを削除して、配列のインデックスを初期化します。

$scope.calculateDistance() では、地図上のマーカー間の距離を計算します。ここでは、Web 上で見つけた計算式を使用しています。この計算処理の結果と座標は、同じ形式を使用しているため、変換処理は必要ありません。
計算後、ons.notification.confirm 要素を使用して、特定のマーカー間の距離 ( Partial Distance ) を表示するか否か、メッセージを表示します。ユーザーの選択肢に応じて、ここでも、ons.notification.alert 要素を使用します。

CSS

Onsen UI がデフォルトで提供するスタイルシート ( CSS ) とは別に、DOM 要素用のスタイルを、新たに追加します。CSS の内容は、style.css をご確認ください。ここでは、Google Maps JavaScript API v3 関連の要素と Onsen UI 関連の要素の表示処理を、適当に行うため、スタイルを新たに追加します。Google Maps JavaScript API v3 と Onsen UI の提供元は異なるため、これらの要素を完全に連携させることはできません。よって、これらの要素を表示するため、2 層のレイヤーを使用します。

下位のレイヤーには、Google Maps の要素 ( 地図、マーカー ) を置き、上位のレイヤーには、Onsen UI の要素を置くことにします。
ここでは、スタイルシートに追加した z-index を使用して、各要素を振り分けます。Maps 関連の要素には、-1 を設定して、下位のレイヤーにプッシュし、Onsen UI 関連の要素には、1 を設定して、上位のレイヤーにプッシュします。

Onsen UI 側の要素の位置を、適切に指定して、Google Maps 側の要素との重複表示を防ぎます。また、要素を適切に配置するためには、画面の解像度も考慮に入れます。要素自体のサイズも重要です。たとえば、div が大きすぎでも、使いにくいマップとなります。



#map_canvas {
    position: absolute; 
    width:100%; 
    height: 100%; 
    margin: 0; 
    padding: 0;   
    z-index: -1
}

.search-input-map{
    width:60%;
    z-index:1;
    margin-left: auto;
    margin-right:auto;
}

.par-search{
    margin-top:10%
}

.par-buttons{
    position: absolute; 
    text-align: center; 
    width:100%; 
    bottom:0px
}

.btn-delete{
    z-index:1
}

.btn-distance{
    z-index:1; margin-left: 5px;


HTML

index.html、menu.html、map.html、settings.html の 4 つのファイルから、このアプリは構成されています ( settings.html の解説は割愛 )。これらのページには、HTML、Onsen UI、AngularJS の要素が記述されています。アプリの大枠には、Onsen UI 提供のスライディングメニューのテンプレートを使用しています。こちらのテンプレートは、Monacaから入手できます。どのテンプレートを使用するかは、開発者側の自由です。DOM の取り扱い方法などに、他のアイデアがある方は、他のテンプレートをご使用ください。

index.html

こちらが、アプリのメインページとなります ( Onsen UI のテンプレートの使用時には、デフォルトで作成されます )。HTML の宣言タグの後に、ng-app="myApp" が記述されています。このディレクティブ ( directive ) を使用して、AngularJS アプリの、いわゆる、自動初期化 ( bootstrap ) を行います。また、このディレクティブは、その役割から、アプリの root 要素を指し示し、通常、ページの root 要素 ( <body> タグ、<html> タグなど ) 付近に置かれます。

スクリプトとシートを記述後、地図を表示するため、Google Mapsスクリプトを記述します。前のバージョンの Google Maps JavaScript API 以降、キーの取得・使用は、任意となりましたが、 Maps API の使用量の監視、追加の割り当ての購入の際には、必要となります。無料の Google Maps API キーの取得・記述方法に関しては、こちらのリンクをご確認ください。

以下のタグを使用して、地図を表示します。



<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js"></script>


body の内容を見ます。body の中には、 <ons-sliding-menu> 要素が、1 つ置かれています。こちらが、アプリ内で使用されている Onsen UI 要素の root になります。また、ここでは、属性をいくつか設定します。 var="slidingMenu" は、AngularJS 内部の 双方向 データ バインディング ( two way data binding ) の設定です。menu-page="menu.html" は、スライディングメニュー用の HTML ページです。main-page="map.html" は、最初に表示するメインページです。また、上述のように、デフォルトでは、メニューのスワイプ表示はできません ( swipeable="false" )。



<!DOCTYPE HTML>
<html ng-app="myApp">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <script src="components/loader.js"></script>
        <link rel="stylesheet" href="components/loader.css">
        <link rel="stylesheet" href="css/style.css">
        <script type="text/javascript"
            src="https://maps.googleapis.com/maps/api/js">
        </script>
        <script src="scripts/controller.js"></script>
    </head>
    <body>
        <ons-sliding-menu swipeable="false" var="slidingMenu" menu-page="menu.html" main-page="map.html" side="left" type="overlay" max-slide-distance="200px">
        </ons-sliding-menu>
    </body>
</html>


menu.html

このページには、Monaca 側で自動作成した、<ons-sliding-menu> 要素用のコードが記述されています。<ons-list> が記述され、その中には、2 つの <ons-list-item> が置かれています。この 2 つの <ons-list-item> を使用して、map.html と settings.html へのページ遷移を、それぞれ行っています。これ以外のページも追加したい場合には、適宜、<ons-list-item> を追加して、ページ遷移を行います。



<ons-page style="background-color: white">
    <ons-list>
        <ons-list-item
            modifier="tappable" class="list__item__line-height"
            onclick="slidingMenu.setMainPage('map.html', {closeMenu: true})">
            <i class="fa fa-home fa-lg" style="color: #666"></i>
             &nbsp; Map
        </ons-list-item>
        <ons-list-item
            modifier="tappable" class="list__item__line-height"
            onclick="slidingMenu.setMainPage('settings.html', {closeMenu: true})">
            <i class="fa fa-gear fa-lg" style="color: #666"></i>
             &nbsp; Settings
        </ons-list-item>
    </ons-list>
</ons-page>


map.html

こちらが、アプリの核となるページです。地図本体と複数の Onsen UI 要素から構成されています。ページ上部で、コントローラーの宣言 ( ng-controller="MapController" ) をしています。コントローラーの宣言は、root 要素で通常行われるので、それ以降の要素は、その影響 ( 設定 ) 下に置かれることが、一目でわかります。

また、その直下には、<ons-toolbar> が設定され、ページのタイトル部の設定と <ons-toolbar-button> が記述されています。<ons-toolbar-button> には、スライディングメニューの開閉を行う記述をします。このため、<ons-toolbar> 内で、別のコントローラー ( ng-controller="SlidingMenuController" ) を宣言しています。

残りのコードは、地図、検索ボックス、2 個のボタン ( <ons-button> ) を表示するためのものです。



<ons-navigator ng-controller="MapController">
   <ons-page>
        <ons-toolbar fixed-style ng-controller="SlidingMenuController">
            <div class="left">
                <ons-toolbar-button ng-click="slidingMenu.toggleMenu()"><ons-icon icon="bars"></ons-icon></ons-toolbar-button>
            </div>
            <div class="center">Map</div>
        </ons-toolbar>

        <div id="map_canvas"></div>

        <p class="par-search">
            <input type="search" class="search-input search-input-map">
        </p>

        <p class="par-buttons">
            <ons-button  class="btn-delete" ng-click="deleteAllMarkers()">
                Delete all markers        
            </ons-button>
            <ons-button class="btn-distance" ng-click="calculateDistance()">
                Distance      
            </ons-button>
        </p>   
    </ons-page>
</ons-navigator>


結論

Onsen UI、Google Maps JavaScript API v3、AngularJS の使用方法を、このサンプルアプリでお見せしました。
このサンプルアプリをひな形として、後は、自由にカスタマイズしてください。Google Maps API では、上述以外でも、たくさんの機能を提供しています。
自作する前に、Google Maps API の詳細を読み、使用できるものがないか、確認しましょう。

ng-japanに参加しました

こんにちは。宇都宮です。今回は、週末に参加したイベントのレポートです。

3/21(土)に、ng-japanという日本初のAngularJSのカンファレンスが開催されました。会場は渋谷マークシティ13Fにある、サイバーエージェントさんのセミナールームでした。

今回のカンファレンスは、スピーカーにAngularJSのコアチームのメンバーが3名いるのが最大の特長でしょう。当然トークは英語なのですが、英=>日の逐次通訳付きなので、英語が苦手な人でも大丈夫なカンファレンスでした。

参加者の最大の関心事は「Angular 2はどうなるのか?」という点にあったと思います。私自身は、今回のカンファレンスに参加して、Angular 2について前向きな気持ちになれました。

以前は、「Angular 1系から2系への移行は無理」「1系はあと2年くらいでサポート終了」と思っていましたが、1系から2系へはアップグレードの道筋がつけられていること、1系のサポート期間はもう少し長くなるかも、といった情報を得ることができました。

プレゼンは全て映像化されていて、youtubeで視聴できます。



以下、各セッションについて簡単にまとめていきます。

Angular 1.4 and beyond



Chirayu Krishnappa(@chirayuk)氏による、AngularJS 1.4と、その周辺のお話でした。

今週にもリリース予定のAngularJS 1.4.0では、いくつかの新機能の追加、パフォーマンス改善とバグフィックスがあり、かつ、1.3系との強い後方互換性があります(破壊的変更は$cookieのみ)。そのため、1.3系のユーザーは、リリース後速やかに1.4.0にアップグレードすべき、とのことでした。

もう1つの話題はAngularJSのコミュニティでした。AngularJSのコミュニティはどんどん成長していて、AngularJSのソースに対するコミットは、現在では、Googleのコアチームよりも、コミュニティによって行われるものの方が多くなっている、とのことでした。

また、AngularJSにコントリビュートするためのお作法や、貢献を必要としている分野(たとえば、すでに解決済みなのに閉じられていないissueを閉じる等)なども紹介されていました。

スライドはhttp://ngjapan2015.chirayuk.com/#/です。

AngularとOnsen UIで作る最高のHTML5ハイブリッドアプリ



弊社の久保田(@anatoo)も、AngularJS製のUIフレームワーク・Onsen UIについて講演しました。



タイトルだけ見るとOnsen UIの紹介がメインのように見えますが、実際は、ハイブリッドアプリの概要から始まり、HTML5アプリのパフォーマンスチューニングの話題が主要な部分を占めます。

ハイブリッドアプリにはUIフレームワークが欠けていた、だからOnsen UIを作ったのだ、という開発の経緯がよくわかるプレゼンでした。

スライドにない部分としては、Onsen UIと同じくハイブリッドアプリ用のUIフレームワークであるionicとの比較について、会場からの質問がありました。

OnsenUIのionicと比べた利点としては以下がある、とのことです。

- (ionicが初期はiOSにフォーカスしていたのに対して) OnsenUIはAndroidでもパフォーマンスの最適化を行っている
- OnsenUI 1.3ではWindows Phoneサポートを追加する予定
- OnsenUIは日本語ドキュメントがある

Routing your way in an Angular app



Brian Ford(@briantford)氏による、AngularJS用の新しいルーターコンポーネント(New Router)のお話でした。

新しいルーターは、以下のリポジトリで開発が進められています。
https://github.com/angular/router

新しいルーターは、Angular 2向けに設計されているものの、Angular 1でも動くように作られています。

また、1系から2系への移行の話題もあり、1系と2系は何らかの手段で共存できるような仕組みが用意され、機能単位で少しずつ移行を進める、といったことができる、とされていました(具体的な移行手順はまだ策定中のようです)。

その他、新しいルーターの機能紹介などが実際のコードを伴って紹介されていました。詳細は以下のスライドを参照してください。
http://goo.gl/YpKc6e

TypeScript+Angular 1.3



わかめ まさひろ氏(@vvakame)による、TypeScriptとAngularJSの関わりについての講演でした。



以前、Angular 2.0はAtScriptという、TypeScriptの機能強化版で実装される、という話がありましたが、最近になって、AtScriptではなくTypeScriptで実装されることになりました。TypeScriptの開発チームとAngularJSの開発チームがディスカッションした結果、TypeScriptに、AngularJSチームが求める機能が入ることになった、という経緯があるらしいです。

Angular 2ではTypeScriptでも開発できるようになるので、早めに慣れておいたほうが良いのでは、ということで、TypeScriptの構文の簡単な紹介から、TypeScript + Angular 1.3におけるプロジェクトのテンプレートの紹介まで、実践的な内容が豊富でした。

ちなみに、一般的なJavaScriptライブラリをTypeScriptで使うには、TypeScript用の型定義ファイルというものが必要になります。Onsen UIには現在、TypeScript用の型定義ファイルが無いのですが、Onsen UI 1.3では型定義ファイルが用意されるとかされないとか…。

Angular 2



Igor Minar(@igorminar)氏による、Angular 2についての講演です。

冒頭、Google内部では2000ものアプリケーションがAngularJSで構築されている、というお話が出てきて、驚きました。社内のみで利用されるアプリケーションがほとんどらしいですが、それにしてもすごい数です。

Angular 2は以下のテーマに基いて構築されているそうです。

- Simpler
- Consistent
- Flexible
- Fast
- Productive

中でも熱心に説明されていたのが、「Fast」、パフォーマンスについてでした。Angular 2は素の状態でもAngular 1の3倍近く、View Cacheを使えば8倍近いパフォーマンス(パフォーマンス最適化の理論上の最適値に近いパフォーマンス)を出すことも可能、とのことです。

また、Angular 1が対応する言語はJavaScriptだけで、AltJSは公式のサポート対象ではありませんでしたが、Angular 2ではTypeScriptがサポート対象に加わります。

今年の5月には、Angular 2.0をGoogle社内で使い始めるらしいです。

また、気になる1系から2系への移行の話ですが、ほとんどの人が1系から2系に移行完了するまでは1系をサポートすること、移行のための機能やツールを用意すること、など、既存ユーザーのための移行パスを設けることを強調していました。

スライドはhttp://goo.gl/plXk7dです。

スポンサーLT



Piece of Cakeの今さんとLIGの林さんのLTのテーマがモロに被っていたのが印象的でした。
どちらも、Angular 1.3から加わった機能・ワンタイムバインディングと、digest loopが走るタイミングについてのプレゼンでした。





要するに、ワンタイムバインディングを使ってwatcherの数を減らすこと、digest loopの数を減らすことは、パフォーマンス改善のために非常に重要である、ということですね。

おわりに



以上、各セッションの内容を簡単にまとめてきました。
AngularJSを実際に作っている人の話が聞けるというのは、とても貴重な経験だったと思います。

1.2リリース!新しく追加されたOnsen UIコンポーネントを紹介します

Onsen UIのバージョン1.2がリリースされました。いくつかの新機能がありますが、今回は特にUI周りで追加されたコンポーネントについて、その実装方法を含めて紹介します。



必要なもの



node.jsおよびnpmが必要です。



Onsen UI 1.2のダウンロード



まずはOnsen UI 1.2系(執筆時点で最新版は1.2.1)をダウンロードします。





Onsen UIの公式サイト



解凍したら、npmを実行します。





$ npm install


これで必要なライブラリがすべてインストールされます。そして、開発用のWebサーバを立ち上げます。





$ gulp serve



サーバを立ち上げると、 http://localhost:8901/index.html をブラウザが開くと思います。これはLiveReloadが入っていますので、HTMLやJavaScriptを編集すればその場で自動リロードが実行されます。





Google Chromeでの表示



アラートダイアログ ons-alert-dialog



アラートダイアログはJavaScript標準で用意されているalert/confirm/promptを置き換えるものです。よりネイティブアプリらしい、フラットなデザインのアラートが表示できます。



www/index.htmlを次のように編集します。





<ons-col width="60px"> 
  <div class="item-thum"></div>
</ons-col>
<ons-col ng-click="showAlert(item)"> <!-- ここに追加 -->
  <header>
    <span class="item-title">{{item.title}}</span>
    <span class="item-label">{{item.label}}</span>
    :



そして www/js/app.jsのDetailControllerを次のように修正します。





module.controller('DetailController', function($scope, $data) {
  $scope.item = $data.selectedItem;
  $scope.showAlert = function(item) {
    ons.notification.confirm({
    message: item.title,
      callback: function (index) {
        switch (index) {
        case 1:
          ons.notification.alert({message: "OKボタンが押されました"});
          break;
        case 0:
          ons.notification.prompt({message: "Cancelを押した理由を教えてください",
            callback: function (text) {
              ons.notification.alert({
                message: text
              });
            }
          });
          break;
        }
      }
    });
  };
});


こうすることで詳細ページにおいて、タイトル部分をタップすると確認ダイアログが出ます。





確認ダイアログ



OKをタップすると、OKボタンが押されたというアラートが出ます。





OKボタンを押した時のアラート



Cancelをタップすると理由を入力するプロンプトが表示された後、入力内容を表示するアラートが表示されるといった具合です。





Cancelをタップした時のプロンプト



メソッド含む詳細はアラートダイアログ ons-alert-dialogでご確認ください。



ダイアログ ons-dialog



アラートダイアログよりもさらにカスタマイズしたUIを実現できるのがons-dialogです。モーダルウィンドウのように使うことができます。



www/index.htmlの中にons-dialogのテンプレートを用意します。下記内容をの上に追加してください。





<script type="text/ons-template" id="alert.html">
  <ons-alert-dialog animation="default" cancelable>
    <div class="alert-dialog-title">Warning!</div>
    <div class="alert-dialog-content">
     An error has occurred!
    </div>
    <div class="alert-dialog-footer">
      <button class="alert-dialog-button">OK</button>
    </div>
  </ons-alert-dialog>
</script>


さらに詳細画面のサムネイル画像部分に対してイベントを設定します。





$scope.showDialog = function (item) {
  ons.createAlertDialog('alert.html').then(function(alertDialog) {
    alertDialog.show();
  });
};


こうすることで、詳細画面のサムネイルをタップするとカスタマイズされたダイアログが表示されます。





ダイアログ表示



メソッド含む詳細はダイアログ ons-dialogでご確認ください。



ポップオーバー ons-popover



ポップオーバーはタップした時にその機能の説明を表示するのに便利です。こちらもまた、www/index.htmlにテンプレートを追加します(の上あたり)。





<script type="text/ons-template" id="popover.html">
  <ons-popover cancelable>
    <p style="text-align: center; opacity: 0.5;">This popover will choose which side it's displayed on automatically.</p>
  </ons-popover>
</script>


さらにツールバー部分を修正します。





<div class="center" id="popover" ng-click="showPopover()">Master Details</div> <!-- ng-click と id を追加します -->


www/js/app.jsのAppControllerの修正をします。





module.controller('AppController', function($scope, $data) {
    :
  $scope.showPopover = function() {
    ons.createPopover('popover.html').then(function(popover) {
      popover.show('#popover');
    });
  };
});


これで準備は完了です。Master Detailsというラベルをタップすると、ポップオーバーが表示されるようになりました。





ポップオーバーの例



メソッド含む詳細はポップオーバー ons-popoverでご確認ください。





最後はカルーセルです。コンテンツをスワイプ操作で左右に切り替えて表示できます。こちらはデザインのみで試せます。 www/index.htmlに以下を追加します。





<ons-page>
  :
  </ons-list>
  <!-- 追加ここから -->
  <ons-carousel swipeable overscrollable auto-scroll var="carousel" style="height: 200px;">
	<ons-carousel-item style="background-color: gray;">
      <div class="item-label">GRAY</div>
    </ons-carousel-item>
    <ons-carousel-item style="background-color: #085078;">
      <div class="item-label">BLUE</div>
    </ons-carousel-item>
    <ons-carousel-item style="background-color: #373B44;">
      <div class="item-label">DARK</div>
    </ons-carousel-item>
    <ons-carousel-item style="background-color: #D38312;">
      <div class="item-label">ORANGE</div>
    </ons-carousel-item>
    <ons-carousel-cover><div class="cover-label">Swipe left or right</div></ons-carousel-cover>
  </ons-carousel>
  <!-- 追加ここまで -->
</ons-page>


このように追加すると、リストの下にカルーセルが表示されます。スワイプ操作でコンテンツが切り替わりますので、写真を見せたり、チュートリアルに使ったりできます。 next()prev()で前後のカルーセルに移動したり、first()last()で最初(または最後)のカルーセルを表示させることもできます。





ルーセル表示の例



メソッド含む詳細はカルーセル ons-carouselでご確認ください。






今回のコードは GitHub上にアップロード してあります。Onsen UIの公式サイトから1.2をダウンロードした後、www以下の内容を差し替えてもらえれば(または www/index.html と js/app.js)確認できるようになっています。



今後もOnsen UIには新しい機能が追加されていきます。ぜひご利用ください!



Onsen UI - A Custom Elements-Based HTML5 UI Framework | Onsen UI

レバレジーズさん主催のヒカ☆ラボPHPイベントで「今、最もイケてるPHPフレームワークLaravel4」の発表をしました。

アシアルの坂本です。
最近は生ハムの原木を買ってしまい塩分過多な生ハム生活をエンジョイしています。

さて、昨日の12月10日にレバレジーズさん主催のヒカ☆ラボPHPイベントに登壇しました。
Laravel4がなぜ熱いか、その根拠と実際の業務で使うベストプラクティス、そしてLaravelの今後についての発表を行いました。

下が実際の発表資料です。



発表後に懇親会で質問を受けましたがLaravel4の導入について不安な方が多くいました。HHVMを使うべきかPhalconに移行すべきか、はたまたパフォーマンスが劇的に改善されるという噂のPHP7を待つべきか、色々な意見がありました。

私としてはLarael4はPHPフレームワークの中で優秀なものであり、かつ使われている技術や考え方は今後、どのフレームワークを使う場合でも必要になるものであると考えています。
実業務でのフレームワーク選定は本当に覚悟のいるものであると思いますがLaravel4を前向きに検討して頂けたらと思います。

また、徐々に全貌が見えてきたLaravel5もその内リリースされると思います。
その時はまた別のイベントで発表させて頂けるかもしれません。

その時はよろしくお願いします。

AngularJS 1.3のフォームまわりの機能強化

こんにちは中川です。

先日、AngularJS 1.3 がリリースされましたね。

動作速度の改善や、メモリ消費量の削減などパフォーマンス面での改善もうれしいところですが、
機能的にはフォーム関連の機能強化が特にうれしく感じましたので、紹介したいと思います。

■ ngModel.$validators



https://docs.angularjs.org/api/ng/type/ngModel.NgModelController

ngModel.$validators を使うと、独自のバリデーション関数を簡単に定義することができるようになりました。

以下の例のように、入力値を引数で受け取り、返り値で真偽値を返す関数を$validatorsオブジェクトに定義します。
$validatorsのキー(ここではvalidCharacters)が、エラーメッセージ表示時などの参照用に利用できます。



ngModel.$validators.validCharacters = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return /[0-9]+/.test(value)  & &
       /[a-z]+/.test(value)  & &
       /[A-Z]+/.test(value)  & &
       /\W+/.test(value);
};


■ ngModel.$asyncValidators



サーバへの問い合わせが必要な場合など、非同期の処理がある場合のバリデーションにも対応しています。
以下の例のように、ngModel.$asyncValidators にpromiseを返す関数を定義します。



ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
var value = modelValue || viewValue;

// Lookup user by username
return $http.get('/api/users/' + value).
   then(function resolved() {
     //username exists, this means validation fails
     return $q.reject('exists');
   }, function rejected() {
     //username does not exist, therefore this validation passes
     return true;
   });
};


今まで、パスワード確認入力やユーザー名重複のサーバ問い合わせなど、
controller(service)の値を組み合わせたバリデーションをdirectiveで
指定するのが面倒で、ui-validateというcontrollerの関数定義を指定できる外部モジュールを利用していました。
ui-vaidateでも今まで対応できていたので、別にそれでもいいといえばいいのですが、
しかし、1.3からは以下のような汎用的なdirectiveを定義すれば、とても素直に指定することができます。



app.directive('appValidators', function () {
    return {
        require: 'ngModel',
        scope: {
            appValidators: '=',
        },
        link: function (scope, elem, attrs, ctrl) {
            var validators = scope.appValidators || {};
            angular.forEach(validators, function (val, key) {
                ctrl.$validators[key] = val;
            });
        }
    };
});

app.directive('appAsyncValidators', function () {
    return {
        require: 'ngModel',
        scope: {
            appAsyncValidators: '='
        },
        link: function (scope, elem, attrs, ctrl) {
            var asyncValidators = scope.appAsyncValidators || {};
            angular.forEach(asyncValidators, function (val, key) {
                ctrl.$asyncValidators[key] = val;
            });
        }
    };
});


利用時は以下のように、controllerでオブジェクトをテンプレート側のapp-validators属性で渡せます。



app.controller('AppCtrl', function($scope) {
	$scope.user_name_validators = {
		hoge: function (modelValue, viewValue) {
			var val = modelValue || viewValue;
			return val == 'hoge';
		},
		fuga: function (modelValue, viewValue) {
			var val = modelValue || viewValue;
			return val == 'fuga';
		}
	};
});



<input type="text" ng-model="user_name" app-validators="user_name_validators">


■ ngMessages



https://docs.angularjs.org/api/ngMessages

フォームのエラーメッセージの表示対応について、
従来のng-showやng-ifで行う方法では、同時に複数エラーが出た場合にもひとつだけ表示するような制御が面倒でしたが、
ngMessagesを利用すると、ずいぶん簡単に記述できるようになりました。

ngMessagesを利用するには別途angular-messages.jsを読み込む必要があります。


<script src="angular.js"></script>
<script src="angular-messages.js"></script>

<form name="myForm">
<input type="text" ng-model="field" name="myField" required minlength="5" />
<div ng-messages="myForm.myField.$error">
  <div ng-message="required">You did not enter a field</div>
  <div ng-message="minlength">The value entered is too short</div>
</div>
</form>


■サンプル



これらの機能を利用したフォームのサンプルを作ってみました。
※ユーザー名:「aaaa」「bbbb」「cccc」を重複エラーとしています。




<!DOCTYPE html>
<html lang="en" ng-app="app">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular-messages.js"></script>
        <script src="app.js"></script>
        <style>
            .container { margin-top: 50px;}
        </style>
    </head>
    <body ng-controller="AppCtrl">

        <div class="container">
            <div class="row">
                <div class="col-xs-6">
                    <form name="userForm" novalidate ng-submit="submit()">
                        <div class="form-group" ng-class="{'has-error': userForm.user_name.$dirty  & & userForm.user_name.$invalid}">
                            <label class="control-label">ユーザー名</label>
                            <span class="help-inline text-danger" ng-messages="userForm.user_name.$error" ng-if="userForm.user_name.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="pattern">不正な値です</span>
                                <span ng-message="minlength">4文字以上</span>
                                <span ng-message="duplicate">既に利用されているユーザー名です</span>
                            </span>
                            <input type="text" class="form-control"
                                   name="user_name" ng-model="user.user_name"
                                   required minlength="4" ng-pattern="/^[a-zA-Z0-9]+$/" app-async-validators="asyncValidators.user_name" />
                            <p class="help-block">※必須、英数字4文字以上</p>
                        </div>

                        <div class="form-group" ng-class="{'has-error': userForm.password.$dirty  & & userForm.password.$invalid}">
                            <label class="control-label">パスワード</label>
                            <span class="help-inline text-danger" ng-messages="userForm.password.$error" ng-if="userForm.password.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="minlength">4文字以上</span>
                                <span ng-message="joe">ユーザー名と一緒はダメー</span>
                            </span>
                            <input type="password" class="form-control"
                                   name="password" ng-model="user.password"
                                   required minlength="4" app-validators="validators.password" />
                            <p class="help-block">※必須、4文字以上</p>
                        </div>

                        <div class="form-group" ng-class="{'has-error': userForm.password_confirm.$dirty  & & userForm.password_confirm.$invalid}">
                            <label class="control-label">パスワード(確認)</label>
                            <span class="help-inline text-danger" ng-messages="userForm.password_confirm.$error" ng-if="userForm.password_confirm.$dirty || userForm.$submitted">
                                <span ng-message="required">必須です</span>
                                <span ng-message="confirm">パスワード確認が一致しません</span>
                            </span>
                            <input type="password" class="form-control"
                                   name="password_confirm" ng-model="user.password_confirm"
                                   required minlength="4" app-validators="validators.password_confirm" />
                        </div>

                        <button class="btn btn-primary btn-block" ng-disabled="userForm.$dirty  & & userForm.$invalid">送信</button>
                    </form>
                </div>
                <div class="col-xs-6">
                    <pre>user = {{user|json}}</pre>
                    <pre>userForm.$error = {{userForm.$error|json}}</pre>
                </div>
            </div>
        </div>

    </body>
</html>




(function() {
    'use strict';

    var app = angular.module('app', ['ngMessages']);

    app.controller('AppCtrl', function ($scope, $q) {
        // モデル
        $scope.user = {};

        // バリデータ
        $scope.validators = {
            password: {
                // ユーザー名とパスワードは一緒はダメ
                joe: function (modelValue, viewValue) {
                    var val = modelValue || viewValue;
                    var user = $scope.user || {};

                    return val != user.user_name;
                }
            },
            password_confirm: {
                // パスワード確認
                confirm: function (modelValue, viewValue) {
                    var user = $scope.user || {};
                    var val = modelValue || viewValue;

                    return val == user.password;
                }
            }
        };

        // 非同期バリデータ
        $scope.asyncValidators = {
            user_name: {
                duplicate: function (modelValue, viewValue) {
                    var users = ['aaaa', 'bbbb', 'cccc'];
                    var val = modelValue || viewValue;
                    return $q(function (resolve, reject) {
                        setTimeout(function () {
                            if (users.indexOf(val) === -1) {
                                resolve('ok');
                            } else {
                                reject('ng');
                            }
                        }, 1000);
                    });
                }
            }
        };

        // user_name != password判定のため
        $scope.$watch('user.user_name', function() {
            $scope.userForm.password.$validate();
        });

        // password == password_confirm判定のため
        $scope.$watch('user.password', function() {
            $scope.userForm.password_confirm.$validate();
        });

        // 送信ボタンイベント
        $scope.submit = function () {
            // 何も変更しないで、送信ボタン時にエラーを表示してあげる
            if ($scope.userForm.$invalid) {
                $scope.userForm.$setDirty();
                return;
            }

            // 成功!!
            console.log($scope.user);
            alert('成功');
        };
    });

    /**
     * validators
     */
    app.directive('appValidators', function () {
        return {
            require: 'ngModel',
            scope: {
                appValidators: '=',
            },
            link: function (scope, elem, attrs, ctrl) {
                var validators = scope.appValidators || {};
                angular.forEach(validators, function (val, key) {
                    ctrl.$validators[key] = val;
                });
            }
        };
    });

    /**
     * asyncValidators
     */
    app.directive('appAsyncValidators', function () {
        return {
            require: 'ngModel',
            scope: {
                appAsyncValidators: '='
            },
            link: function (scope, elem, attrs, ctrl) {
                var asyncValidators = scope.appAsyncValidators || {};
                angular.forEach(asyncValidators, function (val, key) {
                    ctrl.$asyncValidators[key] = val;
                });
            }
        };
    });

})();


今回のバージョンアップで、ますます使いやすくなったと思いますので、
ぜひみなさん試してみてはいかがでしょうか。

Protractorでハイブリッドアプリを自動テスト!

こんにちは、吉田です。


アプリの開発にはテストがつきものですよね。
納品前に最終的なテストを行うのはもちろんですが、それに加えて開発中も気軽にテストを実行できる環境があれば、既存コードのリファクタリングや使用しているフレームワークのバージョンアップなども安心して行えます。


内部のロジックをクラスやメソッド単位で内側からテストするユニットテストの自動化は行われている場合も多いかと思いますが、アプリの場合は画面遷移や要素の表示・挙動が占めるウエイトもかなり大きいので、「Aのボタンを押したらBの画面に遷移し、Cという要素が表示されている」といったような外側からのテストも大事になってきます。
その辺りのテストは手動で行う場合もあるかもしれませんが、フレームワークのバージョンアップやリファクタリングのたびに手動で全ページを表示して動作確認を行うのはつらすぎる。。
外側からのテストも自動化したい・・!!


というわけで今回は
「Cordova + AngularJS + Onsen UI でハイブリッドアプリを作り、それをProtractorで外側から自動テストする」
という内容をご紹介してみたいと思います!


※ちなみに今回アプリを実行するのは実機ではなくブラウザなので、実機で行う厳密な表示テストに替わるものではありませんが、JavaScriptだけで手軽に画面遷移や要素の有無を確認する自動テストを書きたい!という時には役に立つ場面があるかなと思います。


■Cordova + Onsen UIでテスト対象のアプリを作成


まずはテスト対象となる簡単なアプリを作成します。
今回私の記事ではテストを行う部分がメインですので、対象アプリの作成については順を追ったコマンドだけ羅列していきます。
(アプリ作成の段階については別の記事:AngularJS + Onsen UIで始めるPhoneGapアプリケーションで詳しく解説されていますので、ご興味のある方はぜひどうぞ!)


ではターミナルを開いてアプリを作っていきましょう!




# cordovaコマンドが入っていない方のみ
sudo npm install -g cordova

# cordovaアプリの作成
cordova create test-app com.example.testapp TestApp
cd test-app
cordova platform add ios

ここまでで、Cordovaアプリのファイル群ができ、iOSシミュレータや実機で動くようになりました。
シミュレータや実機で動かせる環境がある場合は、cordova run iosで実行できるはずです。


しかし今回の自動テストはPCで立ち上げるブラウザを対象に行いますので、iOSシミュレータや実機の環境がない方でも実行できます!
ではさっそくブラウザで動作確認してみましょう。





cordova serve ios


"Static file server running on port 8000 (i.e. http://localhost:8000)"
というメッセージを確認して、http://localhost:8000 にアクセス。








こんなかんじの画面が出ればOK!
cordova serveをCtrl-Cで停止して、今度は表示する内容をOnsen UIのテンプレートに差し替えてみましょう。
wgetコマンドが入っていない場合は、こちらのzipファイルを落として解凍して手動で差し替えても問題ありません。





wget http://ja.onsenui.io/OnsenUI/project_templates/onsen_master_detail.zip
unzip onsen_master_detail.zip -d onsen
rm -r www
cp -r onsen/www .
rm -r onsen
rm onsen_master_detail.zip


もう一度実行してみます。





cordova serve ios


http://localhost:8000/ios/www/ でこんな画面が表示されて動いていればOK!








これで、テスト対象アプリの準備が出来ました。


■自動テスト環境の準備


今回はブラウザを自動で操作して外側からのテスト、いわゆるエンドツーエンドテストを行ってみたいと思います。
AngularJSがエンドツーエンドテストツールとして推奨している、Protractorというライブラリを使ってみましょう!





npm install protractor # Protractorのインストール
node_modules/.bin/webdriver-manager update # Seleniumツールのインストール


準備ができたら、プロジェクトのルートディレクトリ(test-app/)直下にテスト用のディレクトリを作ります。





mkdir spec
cd spec


ここに、conf.jsというProtractorの設定ファイルを作ります。
内容は以下のとおり。





exports.config = {

  // seleniumサーバーのURL
  seleniumAddress: 'http://127.0.0.1:4444/wd/hub',

  // テストコードを書いたファイル
  specs: [
      'spec.js'
  ],

  // 使用するブラウザ
  capabilities: {
    'browserName': 'chrome'
  }
};


テストを書くファイルは、同じくspecディレクトリの中にspec.jsという名前で作成します。
JSテストフレームワークであるJasmineを使用するので、Jasmineの書き方でテストを記述します。





describe('エンドツーエンドテスト', function() {

    it('最初の画面のテスト', function() {

        // cordova serveのURLを開く
        browser.get('http://localhost:8000/ios/www/');

        // <ons-navigator>タグが表示されている
        expect($('ons-navigator').isDisplayed()).toBe(true);

        // <ons-toolbar>の中のテキストが"Master Detail"と一致する
        expect($('ons-toolbar').getText()).toEqual('Master Detail');
    });
});


specディレクトリの中にconf.jsとspec.jsが準備できましたか?
ではいよいよ実行してみましょう!!


プロジェクトのルートディレクトリ(test-app/)を開いたターミナルの画面を3つ用意してください。
それぞれで





node_modules/.bin/webdriver-manager start # 1つ目のターミナルでSeleniumサーバーを起動
cordova serve ios # 2つ目のターミナルでCordovaアプリをブラウザで実行
node_modules/.bin/protractor spec/conf.js # 3つ目のターミナルでテスト実行


を上から順に実行すると・・・
勝手にChromeが立ち上がって、テストが実行されましたでしょうか?
テストを実行したターミナルに、


1 test, 2 assertions, 0 failures


と表示されていればめでたく成功です!


■テストを増やす


やっと自動でテストを実行できるようになったので、ページ遷移をテストしてみましょう!
リストをクリックして遷移先のページの内容をチェックしていく処理を追記します。





describe('エンドツーエンドテスト', function() {

    it('最初の画面のテスト', function() {

        // cordova serveのURLを開く
        browser.get('http://localhost:8000/ios/www/');

        // <ons-navigator>タグが表示されている
        expect($('ons-navigator').isDisplayed()).toBe(true);

        // <ons-toolbar>の中のテキストが"Master Detail"と一致する
        expect($('ons-toolbar').getText()).toEqual('Master Detail');
    });

    // ----------------- ここから追記 ---------------------

    it('画面遷移 リスト1つ目', function() {
        // リストの1つ目をクリック
        $$('ons-list-item').get(0).click();

        // 画面が変わるのを待つ(1秒)
        waits(1000);
        runs(function() {
            expect($('ons-list-header').getText()).toEqual('Item Information');
            expect($('ons-list-item strong').getText()).toEqual('Item 1 Title');
            $('ons-back-button').click(); // Backボタンをクリック
        });
        waits(1000);
    });

    it('画面遷移 リスト2つ目', function() {
        // リストの2つ目をクリック
        $$('ons-list-item').get(1).click();

        // 画面が変わるのを待つ(1秒)
        waits(1000);
        runs(function() {
            expect($('ons-list-header').getText()).toEqual('Item Information');
            expect($('ons-list-item strong').getText()).toEqual('Another Item Title');
            $('ons-back-button').click(); // Backボタンをクリック
        });
        waits(1000);
    });

    it('画面遷移 リスト3つ目', function() {
        // リストの3つ目をクリック
        $$('ons-list-item').get(2).click();

        // 画面が変わるのを待つ(1秒)
        waits(1000);
        runs(function() {
            expect($('ons-list-header').getText()).toEqual('Item Information');
            expect($('ons-list-item strong').getText()).toEqual('Yet Another Item Title');
            $('ons-back-button').click(); // Backボタンをクリック
        });
        waits(1000);
    });
});


合計4つの画面の遷移と、遷移先ページのテキストのテストは無事に成功しましたでしょうか?
成功するとこんな感じになります!(アニメーションGIF)



画面遷移のときは、画面の切り替えを待つためにclickのあとにwaitsを挟むのがコツです!
ブラウザの画面が自動で動いていくのは面白いですね!


あとは今の冗長なテストコードをリファクタリングしたり、実装に合わせてテストも成長させていきましょう!


■テストの記法について知りたい場合


describe, it, expectなどはJasmineのメソッド
$, getText, clickなどはProtractorのメソッドになります。
どちらも今回使った他にまだまだ機能があり、特にProtractorはAngularJSが推奨しているだけあって、たとえばng-modelやng-bindの値を使って要素を指定することなどもできます。


また、今回はボタンでの画面遷移と要素の確認だけでしたが、他にもポップアップしたアラートの操作やフォームへの入力、スクリーンショットをとることなどもできますので、その辺りも気になる方は以下のドキュメントをご参照ください!


Jasmine(1.3) ドキュメント
Protractor ドキュメント


もしくはリクエストいただければその辺りまで踏み込んだブログを書くかもしれません!


■おわりに


Cordovaアプリの画面遷移チェック自動化について、私が試してみた方法をご紹介させていただきました。


ちなみに今回の記事の手法をひと通り試し終わったあとに見つけたのですが、実機で自動エンドツーエンドテストができるappiumというオープンソースフレームワークもあるようです。
とても気になっているのでそちらも是非試してみたいです!


他にも色々な方法・ノウハウがあるかと思いますので、是非教えていただけると嬉しいです!

AngularJS + Onsen UIで始めるPhoneGapアプリケーション

ハイブリッド モバイル アプリ開発フレームワークであるPhoneGapは、
HTML5でアプリを作るための非常に便利なフレームワークです。
カメラや位置情報などネイティブの機能を簡単にJavaScriptから利用できるようになります。

しかしPhoneGapでは、スマートフォンアプリにとってとても重要な、
UIパーツや画面遷移などのインタラクションは提供されていません。
HTML/CSS/JavaScriptでこれらのUI、アニメーションを一から作るのは非常に大変です。
ネイティブと違和感ないデザインや、なめらかな自然な動き、端末毎の差異など気にすることはたくさんあります。

今回ご紹介する、Onsen UI を使用すれば、PhoneGap/Cordovaアプリを飛躍的に改善することができます。
他のフレームワークよりも洗練された柔軟性を持ち、ハイブリッドアプリにおいても、
ネイティブ並みの外観と操作性を、すべてのプラットフォーム上で実現できます。



この記事では、AngularJSとOnsen UIを利用した
PhoneGapアプリケーションの最小構成のアプリを作っていきます。

※今回は、iOS7テーマを利用していますが、Androidテーマやその他のテーマがあります。
 詳しくはOnsen UIのサイトをご覧ください。

■PhoneGapのインストール・プロジェクト作成





$ sudo npm install -g phonegap
$ cordova create hello com.example.hello HelloWorld
$ cd hello/
$ cordova platform add ios
$ cordova run ios


■Onsen UIの設置



HelloWorldプロジェクトのwwwディレクトリの中身は、今回は利用しないので、一旦全て削除してしまいます。

http://onsenui.io/ の「Download」から一式ダウンロードします。

ダウンロードしたonsen_ui.zipを解凍して、
app/lib/onsen ディレクトリを HelloWorldプロジェクトの www/vendors にコピーします。

その他、今回使うファイルを以下のような構成で作成します。


www/
├── css
│   └── style.css
├── index.html
├── js
│   └── app.js
├── page1.html
├── page2.html
└── vendors
    └── onsen


■画面作成



それでは、www/index.htmlから作成していきましょう。
ons-navigatorタグを page1.html を初期ページとして読み込むように設置しています。



<!DOCTYPE html>
<html lang="en" ng-app="myApp">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <link rel="stylesheet" href="vendors/onsen/css/onsenui.css">
        <link rel="stylesheet" href="vendors/onsen/css/topcoat-mobile-onsen-ios7.css">
        <link rel="stylesheet" href="css/style.css" />
        <script src="cordova.js"></script>
        <script src="vendors/onsen/js/angular/angular.js"></script>
        <script src="vendors/onsen/js/angular/angular-touch.js"></script>
        <script src="vendors/onsen/js/onsenui.js"></script>
        <script src="js/app.js"></script>
    </head>
    <body>
        <ons-navigator page="page1.html" title="Page 1">
        </ons-navigator>
    </body>
</html>


page1.htmlでは、ons-list を使ってitemsをリスト表示しています。
www/page1.html


<ons-page class="center">
    <div ng-controller="Page1Ctrl">
        <ons-list>
            <ons-list-item ng-repeat="item in items" ng-click="next($index)">
                {{item}}
            </ons-list-item>
        </ons-list>
    </div>
</ons-page>


それでは、リストを表示するために、プログラム側を記述します。
データを管理する、Dataサービスを作りましょう。

www/js/app.js


(function() {
    var app = angular.module('myApp', ['onsen.directives', 'ngTouch']);

    app.factory('Data', function() {
        var Data = {};
        Data.items = ['aa', 'bb', 'cc'];
        return Data;
    });

    app.controller('Page1Ctrl', function($scope, Data) {
        $scope.items = Data.items;

        $scope.next = function(index) {
            Data.index = index;
            var item = Data.items[index];
            $scope.ons.navigator.pushPage('page2.html', {title: item});
        };
    });

    app.controller('Page2Ctrl', function($scope, Data) {
        $scope.item = Data.items[Data.index];

        $scope.save = function() {
            Data.items[Data.index] = $scope.item;
            $scope.ons.navigator.popPage();
        };
    });
})();


Page1Ctrlでは、Dataサービスのデータを$scopeにセットしています。
また、$scope.next関数を定義して、リストのアイテムがクリックされた場合に、次の画面に遷移する処理を定義しています。

Page2Ctrlでは、ons-text-inputの値を保存する処理を定義しています。
Saveボタンをクリックされると、Dataサービスのデータを更新して、前の画面に戻ります。

page2.htmlも作成してしまいましょう。



<ons-page class="center">
    <div ng-controller="Page2Ctrl">
        <ons-text-input ng-model="item" style="margin:10px;"></ons-text-input><br>
        <ons-button ng-click="save()">Save</ons-button>
    </div>
</ons-page>


■AngularJSのPhoneGap対応


さて、ここまでで、Onsen UI(AngularJS)でのPhoneGapアプリの見た目はできました。
このままでも動作しているように見えますが、
PhoneGapの機能を使う場合には、deviereadyイベントの後に処理を記述する必要があります。

まず、www/index.htmlのng-app属性を削除してAngularJSが自動起動しないようにします。


変更前: <html lang="en" ng-app="myApp">
変更後: <html lang="en">


次に、devicereadyイベントにてAngularJSの起動を行うように変更します。

www/js/app.js


var app = angular.module('myApp', ['onsen.directives', 'ngTouch']);

document.addEventListener('deviceready', function() {
    angular.bootstrap(document, ['myApp']);
}, false);


これで、コントローラ等の処理の中でも、PhoneGapの命令が使えるようになりました。

■完成



実行して動作を確認しましょう。



$ cordova run ios




今回作成した、wwwディレクトリ一式は「www.zip」よりダウンロードできます。

このようにOnsen UIは、MonacaアプリだけでなくPhoneGapアプリでも利用でき、
簡単にHTML5でネイティブのようなUIのスマートフォンアプリを作ることができます。

Onsen UIはフリーのオープンソースソフトウェアです。
登録も必要なく、簡単に使い始めることができますので、
ぜひ試していただき、色々とフィードバックをいただければと思います。

MonacaバックエンドのAngularJSモジュールを作りました

こんにちは、中川です。

MonacaバックエンドをAngularJSから簡単に利用できるモジュールを作成しました。

Monacaバックエンドとは?



http://docs.monaca.mobi/en/manual/backend_index/

アプリ内でのユーザー管理やプッシュ通知送信を行うには、通常の場合これらの機能を提供するサーバーサイドのプログラムを自分で用意する必要があります。
Monacaバックエンド はクラウドサービスとしてサーバーサイドの機能を提供することで、アプリ開発者がこれらの準備を行わなくても済むようにしています。
すなわち、Monacaバックエンドを使うことで、アプリ開発者はサーバーやサーバーサイドのプログラムを自分自身で用意することなく、バックエンドの機能を使うことができます。

■ AngularJSで利用する場合の問題点



Monacaバックエンドを利用するために、jQueryを利用したJavaScript APIを提供しています。
http://docs.monaca.mobi/ja/reference/javascript/cloud/

このライブラリではjQueryを利用しているため、AngularJSのフレームワークのアプリケーションサイクルの中では少々使いにくいものとなっています。
jQueryajaxクラウドからデータを取得してAngularJSのscopeに渡しても、
ビューが自動的に変わらないため、$scope.$applyをコールするなどの一手間を加える必要があります。
AngularJSの強力なメリットのデータバインディングの良さが半減してしまっています。

monaca-cloud-angular.js



https://github.com/monaca/monaca-cloud-angular

AngularJSのモジュールとして、MonacaバックエンドAPIライブラリをラッピングしています。
オリジナルのMonacaバックエンドAPIとインターフェースはほとんど同じまま、AngularJSで違和感なく利用できるようにしています。

■ 使い方



monaca-cloud-angular.jsをダウンロードします。

Monaca IDEmonaca-cloud.jsを組み込むように設定した、plugin-loader.jsとangular.js(もしくは、Onsen UI)を読み込みます。
※このあたりの設定は、先日のAngularJSのサービスを使ってバックエンドに接続するサンプルを作ってみたをご参照ください。

その後、monaca-cloud-angular.jsをscriptタグで読み込みます。



<!DOCTYPE html>
<html lang="en" ng-app="app">
<head>
    <meta charset="UTF-8">
    <title>Example</title>
    <script src="plugins/plugin-loader.js"></script>

    <script src="js/angular.js"></script>
    <script src="js/monaca-cloud-angular.js"></script>

    <script src="js/main.js"></script>
</head>
<body ng-controller="MainCtrl">

<pre>
{{result|json}}
--------------------------
{{error|json}}
</pre>

</body>
</html>


main.jsで monaca.cloud' モジュールを利用するようにします。
そして、AngularJSのDI機能を利用して、'MonacaBackend'サービスを引数で受け取ります。



// main.js
var app = angular.module('app', ['monaca.cloud']);

app.controller('MainCtrl', function($scope, MonacaBackend) {

    MonacaBackend.User.register('myuser', 'mypass', {'age': 18}).then(
        function(result) {
            console.log(result);
            $scope.result = result;
        },
        function(error) {
            console.log(error);
            $scope.error = error;
        }
    );

});


オリジナル版では、done, failを使う、jQueryのpromiseでしたが、
AngularJSの$qサービスのPromiseを返すようになっていますので、thenを使って成功と失敗の処理を記述します。
$qの詳細については、AngularJSのドキュメント ( http://docs.angularjs.org/api/ng.$q ) を確認してみてください。

■ オリジナル版との違い



オリジナル版との違いは以下となります。



・monaca.cloud => MonacaBackend
・done, fail, always -> then, finally
・monaca.cloud.User._oid => MonacaBackend.User.getOid()
・item.update() => MonacaBackend.CollectionItem.update(item)
・item.getPermission() => MonacaBackend.CollectionItem.getPermission(item)
・item.updatePermission(permission) => MonacaBackend.CollectionItem.updatePermission(item, permission)
・item.delete() => MonacaBackend.CollectionItem.delete(item)


■最後に



AngularJSを組み込んだモバイルUIフレームワークOnsen UIもバージョン1.0となりました。
AngularJSアプリにも簡単に組み込めるMonacaバックエンドも是非利用してみてください。

OnsenUIを使って、Flickrアプリを5分で作ろう

この記事は、英語版アシアルブログの翻訳記事です。
(原文はこちら

==============================

この記事では、Flickr APIを利用して写真を表示するアプリを、HTML5 モバイル UIフレームワークOnsenUIを利用して作成します。

さらに、ボタンのスピナーアニメーションを使うことで、アプリに生き生きとした見た目を与えます。

今回作成するのは以下のアプリです。実際に動くので、触ってみてください。



1. プロジェクトを作成する



1. Monacaダッシュボードで、「新しいプロジェクト」をクリックします。





2. 「Onsen UI最小限のテンプレート」を選択します。





プロジェクトの名前を「Flickr」とし、IDEを起動します。



OnsenUI最小限のテンプレートには、page1.htmlとpage2.htmlを行き来するナビゲーターコンポーネントが含まれています。



2. コードを書く



1. page2.htmlを削除します。



今回作成するのは1ページのアプリなので、page2.htmlは必要ありません。
page2.htmlを右クリックし、「削除」をクリックします。

2. ナビゲーターツールバーのタイトルを変更する



home_navigator.html を開き、 title="Page 1" となっている部分をを title="Flickr" に変更します。

3. 取得したデータを操作するコントローラーを作成する



flickr.js という名前のファイルを www フォルダーに作成し、以下のコードをコピーして貼り付けます。



function FlickrController ($scope) {
 
    $scope.fetchPhotos = function(){
        $scope.failed = false;        
        $scope.isFetching = true;
 
        $.ajax({
            url: "http://api.flickr.com/services/feeds/photos_public.gne?format=json",
            dataType: "jsonp",
            jsonpCallback: 'jsonFlickrFeed',            
            success: function(feeds){
                $scope.$apply(function(){
                    $scope.feeds = feeds;
                    $scope.isFetching = false;
                    $scope.failed = false;
                });
            },
            error: function(error){
                $scope.$apply(function(){
                    $scope.failed = true;                                   
                    $scope.isFetching = false;    
                });
            }
        });
    };
}


4. flickr.js を index.html で読み込む



index.html のタグ内に、以下の記述を追加します。



<script src="flickr.js"></script>




5. page1.htmlを編集し、FlickrControllerが取得したデータを表示する



page1.html を開き、既存のコードを全て消去して、以下のコードに書き換えます。



<div ng-controller="FlickrController">
    <ons-list>
        <ons-list-item class="center">                          
                <ons-button type="cta" should-spin="{{isFetching}}" ng-click="fetchPhotos()">Fetch Photos</ons-button>
        </ons-list-item>
 
        <ons-list-item ng-animate="'zoom'" ng-show="failed">
            Oops! looks like Flickr is down.
        </ons-list-item>
 
        <ons-list-item ng-repeat="item in feeds.items">
            <img width="100%" ng-src="{{item.media.m}}">
        </ons-list-item>
    </ons-list>
</div>


6. 動作確認



ここまでで、コードの作成は終了です。Monaca IDEの「プレビュー」機能やMonaca Debuggerで、動作確認してみましょう。

7. FlickrControllerとpage1.htmlの結びつき



OnsenUIは、AngularJSをベースに作られています。AngularJSでは、コントローラーと、HTML要素を、ng-controllerで結びつけています。コントローラーは、以下の図で示されているように、$scopeオブジェクトを介してデータをDOMに渡しています。



1. FlickrControllerという名前の関数をコントローラーとして作成し、$scopeをこの関数に渡しました。$scopeを介して、DOMにデータを渡すことができます。DOMとコントローラーを、ng-controllerで結びつけています。

2. ボタンがクリックされると、$scope.fetchPhotos()が呼ばれます。

3. Flickrからデータを取得している間、ボタンにスピナーアニメーションが表示されます。

4. feed.items はFlickr APIから取得した配列です。ng-repeatを使って、配列の全要素を表示します。

5. ng-showを使って、APIの呼び出しが失敗した際のエラーメッセージを表示しています。

3. まとめ



OnsenUIを使うと、スムーズなアニメーション等のリッチなUIを備えたアプリを、簡単に作ることができます。

ng-controllerによって、データを簡単にDOMに渡すことができます。

4. ソースコード全文



index.html



<!doctype html>
<html lang="en" ng-app="myApp">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <link rel="stylesheet" href="plugins/plugin-loader.css">     
  <script type="text/javascript" src="plugins/plugin-loader.js"></script>
 
  <script src="flickr.js"></script>
 
  <script>
    angular.module('myApp', ['onsen.directives']);
 
    document.addEventListener("deviceready", onDeviceReady, false);
 
    function onDeviceReady() {
        // Now safe to use the Cordova API
    }
  </script>    
</head>
 
<body>
 
  <ons-screen page="home_navigator.html"></ons-screen>
 
</body>
</html>


home_navigator.html



<ons-navigator title="Flickr" page="page1.html"></ons-navigator>


flickr.js



function FlickrController ($scope) {
 
    $scope.fetchPhotos = function(){
        $scope.failed = false;        
        $scope.isFetching = true;
 
        $.ajax({
            url: "http://api.flickr.com/services/feeds/photos_public.gne?format=json",
            dataType: "jsonp",
            jsonpCallback: 'jsonFlickrFeed',            
            success: function(feeds){
                $scope.$apply(function(){
                    $scope.feeds = feeds;
                    $scope.isFetching = false;
                    $scope.failed = false;
                });
            },
            error: function(error){
                $scope.$apply(function(){
                    $scope.failed = true;                                   
                    $scope.isFetching = false;    
                });
            }
        });
    };
}


page1.html

>>HTML



Fetch Photos



Oops! looks like Flickr is down.