[salesforce][SPA][AngularJS] AngularJS+force.comでSingle Page Application
さて、前回の[salesforce][SPA] force.comでSingle Page Applicationの続きとして今回はAngularJSを活用する例をご紹介します。
作成するアプリケーションの仕様としては取引先の一覧画面があって任意の取引先名をクリックすると詳細画面が開くという、Salesforce標準のViewから詳細画面のような流れを実装してみます。
まず、フロントエンドだけで画面遷移するように以下のようにコーディングします。AngularJS自体もCDNから読み込むため本当に以下3ファイルを作成するだけで動きます。
main.page
[html]
<apex:page>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
<script type="text/javascript">
var accounts = [{
"Name": "dummy1",
"BillingPostalCode": "105-0001",
"BillingState": "東京都",
"BillingCity": "港区",
"BillingStreet": "虎ノ門4-1-8 虎ノ門4丁目MTビル6F",
"Phone": "03-6895-1520"
}, {
"Name": "dummy2",
"BillingPostalCode": "105-0001",
"BillingState": "東京都",
"BillingCity": "港区",
"BillingStreet": "虎ノ門4-1-8 虎ノ門4丁目MTビル6F",
"Phone": "03-6895-1525"
}];
var app = angular.module(‘ngApp’, []);
app.config(function ($routeProvider) {
$routeProvider.
when(‘/’, {
controller: ‘listCtrl’,
templateUrl: ‘/apex/list’
}).
when(‘/view/:id’, {
controller: ‘detailCtrl’,
templateUrl: ‘/apex/detail’
}).
otherwise({
redirectTo: ‘/’
});
});
app.controller(‘listCtrl’, [‘$scope’, function($scope) {
$scope.accounts = accounts;
}]);
app.controller(‘detailCtrl’, [‘$scope’, ‘$routeParams’, function($scope, $routeParams) {
$scope.account = accounts[$routeParams.id];
}]);
</script>
<div ng-app="ngApp">
<div class="ng-view"></div>
</div>
</apex:page>
[/html]
list.page
[html]
<apex:page sidebar="false" showHeader="false">
<div ng-controller="listCtrl">
<table>
<tr>
<th>取引先名</th>
<th>市区郡</th>
<th>電話番号</th>
</tr>
<tr ng-repeat="account in accounts">
<td>
<a href="#/view/{{$index}}">{{account.Name}}</a>
</td>
<td>{{account.BillingCity}}</td>
<td>{{account.Phone}}</td>
</tr>
</table>
</div>
</apex:page>
[/html]
detail.page
[html]
<apex:page sidebar="false" showHeader="false">
<div ng-controller="detailCtrl">
<table>
<tr>
<td>取引先名</td>
<td>{{account.Name}}</td>
</tr>
<tr>
<td>郵便番号</td>
<td>{{account.BillingPostalCode}}</td>
</tr>
<tr>
<td>住所</td>
<td>{{account.BillingState}}{{account.BillingCity}}{{account.BillingStreet}}</td>
</tr>
<tr>
<td>電話番号</td>
<td>{{account.Phone}}</td>
</tr>
</table>
</div>
</apex:page>
[/html]
画面遷移を定義するには上記のように$routeProviderを使ってアクセスされるpath毎にコントローラとテンプレートを指定します。この時のpathは実際のURLのハッシュフラグメント(URLの「#」以下の文字列)を指します。
list.pageにあるng-repeat=”account in accounts”にてaccountsの中身がリピートされaccountとしてアクセスできるのは何となく解るかと思いますが、ng-repeatにはリンク先のドキュメントにある通りループ変数が用意されていて、今回は$indexを利用してModelの要素を特定するような作りにしました。
さらに、詳細画面(detailCtrl)ではURLパラメータを取得できる$routeParamsを使って、一覧画面でクリックされた要素のインデックスを取得するようになっています。
あとはダミーデータの部分に取引先オブジェクトのレコードをJSONでぶち込んでやれば良いのですが、ここで以前利用したRemoteTKを参考にJavascript RemotingでApexからAccountのJSONを返してもらうようにコーディングしてみます。
まずはApexコントローラを以下のように準備します。
ngController.cls
[html]
public with sharing class ngController {
@remoteAction
public static String loadAccounts() {
List<Account> accounts = [
Select
Name,
BillingPostalCode,
BillingState,
BillingCity,
BillingStreet,
Phone
From Account Limit 10
];
return JSON.serialize(accounts);
}
}
[/html]
これは説明不要でしょうが単純にAccountを10件取得してJSONにして返しています。
そしてmain.pageを以下のように修正します。
main.page
[html]
<apex:page controller="ngController">
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js"></script>
<script type="text/javascript">
var accounts;
var app = angular.module(‘ngApp’, []);
app.config(function ($routeProvider) {
$routeProvider.
when(‘/’, {
controller: ‘listCtrl’,
templateUrl: ‘/apex/list’
}).
when(‘/view/:id’, {
controller: ‘detailCtrl’,
templateUrl: ‘/apex/detail’
}).
otherwise({
redirectTo: ‘/’
});
});
app.factory(‘vfr’, function() {
return {
loadAccounts: function(callback, error) {
var deferred = $.Deferred();
Visualforce.remoting.Manager.invokeAction(‘ngController.loadAccounts’, function(result) {
deferred.resolve(JSON.parse(result));
}, {
escape: false
});
return deferred.promise();
}
}
});
app.controller(‘listCtrl’, [‘$scope’, ‘vfr’, function($scope, vfr) {
var loadAccounts = vfr.loadAccounts();
loadAccounts.then(function(records) {
accounts = records;
$scope.accounts = accounts;
if(!$scope.$$phase) {
$scope.$digest();
}
});
}]);
app.controller(‘detailCtrl’, [‘$scope’, ‘$routeParams’, function($scope, $routeParams) {
$scope.account = accounts[$routeParams.id];
}]);
</script>
<div ng-app="ngApp">
<div class="ng-view"></div>
</div>
</apex:page>
[/html]
apex:pageの属性に先ほど作成したngControllerを指定し、factoryを使ってJavascript RemotingをService化します。Service化したサービスは上記のようにAngularJSのコントローラで使用可能になります。
※上記のソースはSitesにて動作確認できます。
これでフロントエンドとサーバサイド(force.com)をつなげることが可能になります。
個人的にはSalesforceの標準機能プラスαな開発が多いforce.com開発には、AngularJSのような中小規模のアプリケーションに適したフレームワークがピッタリだと思っています。