CFCのクエリ結果をAngularJSが読めるフォーマットに変換する

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

フロントエンドにAngularJS使う機会が増えてきたので、勉強ついでに昔作ったWebアプリのフロントをAngularJSに置き換えてみようと思いAngularJSを触ってみました。

まずは以下のクエリをサンプルに使ってみます。(これはCFMLで返り値の内容を確認するためにtest.cfmとして保存)

<cfquery name="EmpList" datasource="cfdocexamples" result="tmpResult"> 
    SELECT FirstName, LastName, Salary, Contract 
    FROM Employee 
</cfquery>
<cfdump var="#EmpList#">

↓実行結果のdump

angularjs_cfc_1

下記のサンプルコードをblog.cfcとして作成します。

<cfcomponent>
    <cffunction name="getEmpList" access="remote" returntype="query">     
		<cfquery name="EmpList" datasource="cfdocexamples" result="tmpResult"> 
		    SELECT FirstName, LastName, Salary, Contract 
		    FROM Employee 
		</cfquery>
        <cfreturn EmpList>    
    </cffunction>
</cfcomponent>

次に作成したCFCが正しく動作するかを確認します。(test.cfmを下記コードに置き換えてblog.cfcを呼び出します)

<cfinvoke component="blog" method="getEmpList" returnVariable="EmpList">
<cfdump var="#EmpList#">

結果は上で実行したものと同じなので割愛します。サーバ側の前準備は整いましたので、次はフロントを準備します。

フロント側は検索ボタンを置いてクリックするとblog.cfcのgetEmpListを呼び出してD/Bから取得した結果を表示する。というとてもシンプルなものを用意します。(blog_angular.html)
AngularJSを使ってUIを作る時には便利なUI bootstrapがあるので今回のサンプルではページネーション部分に使っています。

<!DOCTYPE html>
<html lang="ja" ng-app="sampleSearchApp">
<head>
    <meta charset="utf-8">
    <title>AngularJS + CFC サンプル画面</title>
    <meta name="viewport" content="width=device-width, initial-scale=0.95">
    <link href="css/bootstrap.min.css" rel="stylesheet" media="screen">
    <link href="css/style.css" rel="stylesheet" type="text/css" >

    <script src="js/angular.js"></script>
    <script src="js/blog_angular.js"></script>
    <script src="js/ui-bootstrap-tpls-0.10.0.js"></script>
</head>

<body>

<div class="container">
    <div id="header" class="row">
        <center><h3>AngularJS + CFC サンプル画面</h3></center>
    </div>

    <!-- 検索結果一覧を表示するエリア -->
    <div class="table-responsive" ng-controller="SearchResultCtrl">
        <div class="row">
            <div class="col-sm-3">
              <br />
                該当件数は <strong>{{emps.length}}</strong> 件
            </div>
        </div>
        <div class="row">
            <div class="col-sm-1 col-sm-offset-10">
                <button type="submit" class="btn btn-info" ng-click="$emit('GoSearch')">検索実行</button>
            </div>
        </div>
      <table class="table table-hover table-bordered table-striped">
        <thead>
        <tr>
          <th>No</th>
          <th>FirstName</th>
          <th>LastName</th>
          <th>Salary</th>
          <th>Contract</th>
        </tr>
      </thead>
      <tbody>
        <tr ng-repeat="emp in filteredEmps">
          <th>{{$index+1}}</th>
          <th>{{emp.firstname}}</th>
          <th>{{emp.lastname}}</th>
          <th>{{emp.salary}}</th>
          <th>{{emp.contract}}</th>
        </tr>
      </tbody>
      </table>
      <pagination page="currentPage" items-per-page="perpage" total-items="totalItems"></pagination>
    </div>
</div>
<script src="js/jquery.min.js"></script>
<script src="js/bootstrap.min.js"></script>

</body>
</html>

次はjavascript側を用意します。(blog_angular.js)
UI bootstrapの機能を使いたいので、1行目でangular.moduleを宣言しています。

angular.module('sampleSearchApp', ['ui.bootstrap'])

function SearchResultCtrl($scope,$http,$log){
  $scope.filteredIssues = [];
  $scope.currentPage = 1;
  $scope.numPerPage = 5;

  $scope.$on('GoSearch',function(){
    $scope.emps = [];

    $http.get('blog.cfc?method=getEmpList&returnformat=json')
      .success(function (response) {
          if (angular.isArray(response)) {
            $scope.emps = response;
            $scope.totalItems = parseInt(response.length,10);                    
            $scope.perpage = 5;

            $scope.$watch('currentPage + numPerPage', function() {
              var begin = (($scope.currentPage - 1) * $scope.numPerPage)
              , end = begin + $scope.numPerPage;
            $scope.filteredEmps = $scope.emps.slice(begin, end);
            });
          } else {
            console.log("response is not Array!");
            $scope.totalItems = 0;
            $scope.emps=[];
            $scope.filteredEmps =[];
          }
    })
      .error(function (data, status, headers, config) {
        $log.log("data : ", data);
        $log.log("status : ", status);
        $log.log("headers : ", headers);
        $log.log("config : ", config);
    });
});
}

早速、上で作成したHTML、JS、CFCをつかって実行してみましたが残念ながら何も表示されませんでした。デベロッパーツールで見てみるとレスポンス自体は返ってきているようですが、AngularJS側がうまく読めるフォーマットになっていないことが原因のようです。

blog_angular.jsの13行目のangular.isArray(response)でfalseが返ってきていました。

angularjs_cfc_4

ということで手っ取り早く動かすためにcfqueryで返ってくるクエリレコードセットをAngularJSが読める下記JSONフォーマットに変換することにします。

[
{"key":"value","key":"value","key":"value","key":"value"},
{"key":"value","key":"value","key":"value","key":"value"},
{"key":"value","key":"value","key":"value","key":"value"},
];

先ほど作成したblog.cfcを改良します。

  1. 2行目でcffunctionのオプションで定義しているreturntype="query"をreturntype="any"にしました。
  2. 7行目でクエリー結果をJSONに変換するためにcf2ngで変換させます。
  3. 11行目以降のcffunctionでcf2ngの処理を記述しました。
<cfcomponent>
    <cffunction name="getEmpList" access="remote" returntype="any">
		<cfquery name="EmpList" datasource="cfdocexamples" result="tmpResult">
		    SELECT FirstName, LastName, Salary, Contract
		    FROM Employee
		</cfquery>
        <cfreturn cf2ng(EmpList)>
        <!--- <cfreturn EmpList> --->
    </cffunction>

    <cffunction name="cf2ng" access="remote" returntype="string">     
        <cfargument name="data" type="any" required="Yes" />

        <!--- Variable Decralation --->
        <cfset var jsonString = "" />

        <cfset columnListName = arrayNew(1)>

        <!--- Query Column Name to Set Array --->
        <cfloop index="lpc" from="1" to="#listLen(data.columnList, ',')#">
            <cfset columnListName[lpc] = LCase(listGetAt(#data.columnlist#, #lpc#, ","))>
        </cfloop>

        <cfset jsonString = "[">

        <cfloop query="data">
          <cfset jsonString = #jsonString# &  '{"'>
          <cfloop index="lpcnt" from="1" to="#listLen(data.columnList, ',')#">
                <cfset jsonString = #jsonString# & columnListName[lpcnt] & '":"'>
                <cfset tmpvalue = #data[columnListName[lpcnt]][data.currentRow]#>

                <cfif lpcnt eq #listLen(data.columnList, ',')#>
                    <cfset jsonString = #jsonString# & #tmpvalue# & '"},'>
                <cfelse>
                    <cfset jsonString = #jsonString# & #tmpvalue# & '","'> 
                </cfif>
          </cfloop>
        </cfloop>

        <cfset jsonString =  mid(#jsonString#, 1, len(jsonString)-1) & "]">

        <cfreturn jsonString>
    </cffunction>
</cfcomponent>

ページ切り替えもうまく動きました。その実行結果のスクリーンショットです
検索結果をよく見るとJeremy Allaireが入ってるのに今更気づきましたw

angularjs_cfc_3

まとめ

一応cf2ngは汎用的に作ったつもりなので、cfqueryのクエリー結果であればそのままAngularJSが読めるJSONフォーマットに変換できると思います。
変換をかけるのでレコードセットが大量だとレスポンスに難ありかもです。