tokuhirom's Blog

AngularJS を本気でつかうための tips

最近、管理画面で AngularJS をつかってみている。 そんな中で、いくつか工夫した点があるのでそれをシェアさせていただきます。

XHR のエラーを表示する

XHR のエラーがおきた際のハンドリングをいちいち手でかくのは非効率。管理画面とか中の人しかつかわないので、エラーがおこった旨を随時報告するだけでよい。

そんなケースでは以下のようにする。

angular.module('myapp.exceptionHandler', [])
.config(['$httpProvider', function ($httpProvider) {
  $httpProvider.interceptors.push(function($q, $log, $rootScope) {
    return {
      'responseError': function(response) {
        $log.error(response);

        var h = $(response.content).find('html');
        $rootScope.$broadcast('showExceptionDialog', response);
        $q.reject(response);
      }
    };
  });
}])
.controller('ExceptionDialogCtrl', function ($scope, $http, $rootScope) {
  $rootScope.$on('showExceptionDialog', function (event, err) {
    $scope.err = err;
    $('#errorDialog').modal();
  });
});

$http で XHR のエラーが発生した場合、bootstrap のモーダルを表示するようにしている。便利。

やや乱暴だけど。

X-Requested-With ヘッダを送出する

X-Requested-With ヘッダをつけたいな、ってときは以下のようにしたらよい。

angular.module('myapp.exceptionHandler', [])
.config(['$httpProvider', function ($httpProvider) {
  $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHTTPRequest';
}]);

ディレクトリ構成をそれなりに考える

app/
  controller/
    entry.js
    comment.js
  app.js

上記のようにするとよいかなあ、とおもっています。今回は、既存の AngularJS じゃないところから AngularJS にかえようとしているのでこんな綺麗なかんじになってなくて、CRUD 対象のテーブルごとに App を定義してるんですけどね!

Pagination について

<table>
  <tr ng-repeat="row in rows">
    <td>{{row.id}}</td>
    <td>{{row.name}}</td>
  </tr>
</table>
<a class="btn btn-default btn-block" ng-click="read_more()" ng-show="has_next" >Read More</a>

みたいにしておいて、

.controller('ctrl.foo.index', function (api, $scope) {
  $scope.rows = [];
  $scope.page = 1;

  $scope.query = function () {
    api.query({page: $scope.page}, function (dat) {
      $scope.rows = $scope.rows.concat(dat.rows);
      $scope.has_next = dat.has_next;
    });
  };
  $scope.query();

  $scope.read_more = function () {
    $scope.page++;
    $scope.query();
  };
});

などとすればよい。

Pagination の方式にはいろいろあるが、基本的にはこういう風に実装するのが楽。

API アクセスについて

管理画面用に JSON API を用意している。管理画面用に用意した JSON API をコールするのに最初は ngResource をつかっていたのだが、いかんせんめんどくさい。ngResource をつかっていると ngResource の流儀にあわせなければならないし、ちょっとでも rail をはずれると、もうめんどくさくてしょうがない。おしつけがましい。

そして、ngResource のインスタンスを共通化しようと思うと DI で導入することになるわけだけど、それをいちいち引数でうけとるのがまぁダルい。

そういうわけで、$http をベースに自分でかきなおした。(ぶっちゃけ jQuery の ajax 機能でかいてもよかった)

https://github.com/tokuhirom/angular-api-client.js にそのソースを公開しています。

以下のように API client を定義しておく。

angular.module('myapp.api', ['AngularAPIClient'])
.factory('api', function (AngularAPIClient) {
  return {
    entry: apiClient.make([
      ['query',  'GET',  '/entry/query'],
      ['create', 'POST', '/entry/create'],
    ]),
    member: apiClient.make([
      ['query',  'GET',  '/member/query'],
    ])
  };
});

これは以下のように使用することができる。

angular.module('myapp', ['myapp.api'])
.controller('EntryCtrl', function (api) {
  api.entry.query({}, function (dat) {
    $scope.rows = $scope.rows.concat(dat.rows);
  });
});

ライブラリのコード自体も30行ぐらいしかないので、よめばすぐわかるのでよい。

なお、API Client のライブラリはサーバーのルーティングコードから自動生成している。

resource 'entry', 'Entry', sub {
  get  'query';
  post 'create';
};

とか書いておくと、サーバーサイドでは MyApp::Admin::C::Entry#query に /entry/query がマッピングされる、みたいなかんじ。そして、ついでにこの DSL から api-client.js も生成している的な!!

まとめ

いくつかの tips をのべさせていただきました。

追記

AngularAPI 的には $ はリザーブドだと tmatsuo さんにきいたので $ つけるのやめた!