AngularJSで投票システム(サムアップカウンタ)を開発する
画面遷移の無い投票インタフェースからREST API経由でカウントアップ
AngularJSのサンプルアプリとして投票カウンターを作る。
完成画像は以下のようになった。
HTMLに
- ZURB Foundation
- Foundation Icon Fonts 3
でデザインをプラスしている。サムアップとサムダウンのアイコンは、Icon Fontsのものを利用した。当初、ベースとなるHTMLは以下のようにした。
<div class="panel"> <ul class='inline-list'> <li><a><i class='fi-like'></i></a></li> <li><span class='round success label'>0</span></li> <li><a><i class='fi-dislike'></i></a></li> <li><span class='round alert label'>0</span></li> </ul> </div>
基本的な見た目は、上のHTMLで完成なのだが、このHTMLをAngularJSを利用して以下のように変更した。
<div ng-repeat="e in thumbs" ng-class="{callout: e.Thumb.callout}" class="panel"> <ul class='inline-list'> <li><a ng-click='addGood($index)' ng-mouseover="hovG = true" ng-mouseleave="hovG = false"><i class='fi-like'></i></a></li> <li><span ng-class="(hovG==true) ? 'regular' : 'success'" class='round label' ng-model='e.Thumb.good'>{{e.Thumb.good}}</span></li> <li><a ng-click='addBad($index)' ng-mouseover="hovB = true" ng-mouseleave="hovB = false"><i class='fi-dislike'></i></a></li> <li><span ng-class="(hovB==true) ? 'regular' : 'alert'" class='round label' ng-model="e.Thumb.bad">{{e.Thumb.bad}}</span></li> </ul> </div>
上のHTMLからAngularJSの機能をピックアップしてみると、
- ng-repeat
- ng-class
- ng-click
- ng-mouseover
- ng-mouseleave
- ng-model
のディレクティブを利用しており、これらの機能を利用して、バックエンド側と連携した、ユーザーアクションに応じてフロント側のデザインも動的に変化する投票インタフェースになっている。
サムアップのアイコンフォントをクリックすると、その右の数字が1つづつ増えていく。数字のカウント部分はバックエンド側のDBを更新する仕様にしており、REST APIはCakePHPを利用する。カウントされた行については、calloutのclass属性をAngularJSのng-classを利用して動的に追加されるようになっている。
また、アイコンフォント上にマウスオーバーした際に数字部分の色を変化するようにしている。今回の投票カウンターでは、フロント側で色を変えるだけの処理になっているが、マウスオーバーなどのユーザーアクションでバックエンド側と連携する処理もAngularJSでは、効率的に開発することができる。
そして、ng-modelを利用して$scopeのデータを常時バインディングしているため、画面遷移することなくカウントアップしたデータがリアルタイムに反映されると投票カウンターになった。
ng-clickディレクティブには、ユーザーのクリックアクションで実行されるメソッドが関連付けてあるのだが、その前にページを表示した際の一覧取得から見てみようと思う。
AngularJSのControllerとServiceの作成
一覧の取得
最初にページを表示した際の投票データの一覧取得処理について書く前に、簡単にルーティング処理についてふれておきたい。
以下のコードをapp.jsで定義して、
/thumb
というURLでブラウザからアクセスできるようにしている。そして、GetThumbCtrlというコントローラを関連付け、thumb.htmlをviewとして利用するように指定している。
ajs.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { $locationProvider.html5Mode(true); $routeProvider .when('/thumb', { templateUrl: 'app/partials/thumb.html', controller: 'GetThumbCtrl' }) .otherwise({ redirectTo: '/' }); } ]);
投票データの一覧を取得するGetThumbCtrlは以下のようにした。
ajs.controller('AppCtrl', function($scope, $rootScope, $http, $location) { $rootScope.appUrl = "http://restful-cakephp.local"; }); ajs.controller('GetThumbCtrl', function(GetThumbService, $scope) { GetThumbService.async().then(function(d) { $scope.thumbs = d.data.thumbs; }); });
コントローラでは、$scopeにデータをまとめてview側でデータを利用できる状態にしてある。AppCtrlは、上位のHTMLタグに設定し、AngularJSアプリ内でグローバルに利用するデータをセットするために使用する。実際にバックエンド側からデータを取得する処理は、
GetThumbService
というService側で処理するようにしてある。そのGetThumbServiceは以下のとおり。
ajs.factory('GetThumbService', function($http, $rootScope) { var GetThumbService = { async: function() { var promise = $http.get($rootScope.appUrl + '/thumbs.json') .success(function(data, status, headers, config) { return data; }); return promise; } }; return GetThumbService; });
上のServiceによりサーバ側にリクエスト処理が行われデータを取得することができる。上で載せたコードのように
- app.js
- controller.js
- service.js
それぞれのファイルに処理を分けて書くことで、コードを構造化することができる。
REST API
バックエンドのAPIは、CakePHPで作成する。今回は、
- 一覧取得
- カウントデータの更新
だけを実装した。
public function index() { $thumbs = $this->Thumb->find('all'); $this->set(array( 'thumbs' => $thumbs, '_serialize' => array('thumbs') )); } public function edit($id) { $this->Thumb->id = $id; if( $this->request->data['good'] ) { $field = "good"; $old = $this->request->data['good']; $new =intval($this->request->data['good']) + 1; } else { $field = "bad"; $old = $this->request->data['bad']; $new =intval($this->request->data['bad']) + 1; } if ($this->Thumb->saveField($field, $new)) { $message = 'Saved'; $new_value = $new; $callout = true; } else { $message = 'Error'; $new_value = $old; $callout = false; } $this->set(compact('message', 'new_value', 'callout')); $this->set('_serialize', array('message', 'new_value', 'callout')); }
渡された引数に応じて、サムアップなのかサムダウンなのかを判定した上でsaveFieldに渡すパラメータを設定しデータベースに保存を実行している。最後にフロント側に戻すデータをcompactしてからserializeでjson化してレスポンスしている。
データベース側の構造は以下のようにした。
CREATE TABLE IF NOT EXISTS `thumbs` ( `id` int(11) NOT NULL AUTO_INCREMENT, `tid` int(11) NOT NULL, `good` int(11) NOT NULL DEFAULT '0', `bad` int(11) NOT NULL DEFAULT '0', `total` int(11) NOT NULL DEFAULT '0', `ip` varchar(256) NOT NULL, `created` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
カウントアップ
カウントアップ処理は、ユーザーのクリック操作により始まる。本エントリーの上の方に載せたAngularJSのHTMLで
ng-click='addGood($index)'
ng-clickディレクティブにaddGoodというメソッドを関連付けた。このaddGoodをGetThumbCtrlに追加することで実行可能な状態とすることができる。
$scope.addGood = function(index) { AddGoodThumbService.async($scope.thumbs[index].Thumb).then(function(d) { $scope.thumbs[index].Thumb.good = d.data["new_value"]; $scope.thumbs[index].Thumb.callout = d.data["callout"]; }); }
addGoodメソッドでも$scopeのデータ構造の整備を行っている。データ構造を整えておくことで、view側のng-modelで同じ$scopeを設定することにより、ユーザーのクリック操作によりカウントアップしたデータがダイレクトに反映される仕組みになっている。
一覧取得と同じ要領で、AddGoodThumbServiceが実際のバックエンドとの連携部分を担当する。
ajs.factory('AddGoodThumbService', function($http, $rootScope) { var AddGoodThumbService = { async: function(data) { var _data = {}; _data.good = data.good; var promise = $http.put($rootScope.appUrl + '/thumbs/' + data.id + '.json', _data) .success(function(data, status, headers, config) { return data; }); return promise; } }; return AddGoodThumbService; });
カウントアップでは、httpのputメソッドを利用してバックエンド側に更新リクエストを行っている。getの場合と異なり引数を設定して現在のカウントデータを渡している。
AddGoodThumbServiceをコントローラ側から利用可能な状態にするために、GetThumbCtrlで以下のようにAddGoodThumbServiceを利用可能な状態にする必要があることを補足しておく。
ajs.controller('GetThumbCtrl', function(GetThumbService, AddGoodThumbService, $scope) { });
サムアップ側の処理は上の設定を行うことで動作する。addBad側については、AddBadThumbServiceというサムダウン専用のServiceを作成してgoodの場合と同じ要領で実装することができる。