AngularJSとRailsを用いたモーダル画面でCRUDアプリケーション


モーダル画面を使ってCRUDするアプリケーションをAngularJS+Railsで作成する方法を解説します。

本エントリにあたり、[SrcHndWng/AngularjsWhiskyList](https://github.com/SrcHndWng/AngularjsWhiskyList)を全面的に参考にさせていただきました。ありがとうございます。
また、本エントリのソースはこちらで公開しております。

[jwako/angular_crud_app](https://github.com/jwako/angular_crud_app)

## AngularJS(フロントサイド)とRailsの役割分担と実装パターン
- まず、モーダル画面を使って(AngularJSに限らずとも)、RailsでCRUDアプリケーションを作成する場合、フロントサイドとサーバーサイドで、誰が何をどこまでやるかを決めないといけません。
- **<何を>**を分解すると以下のようになります。
	- Viewのレンダリング
	- 入力フォームのバリデーション
	- リクエスト処理
	- CRUD処理
	- レスポンス処理(リダイレクト、エラーハンドリングなど)

- いくつか実装方法はあると思いますが、Railsの役割に注目すると、次の大きく次の2つのやり方があると思います。
- 本エントリでは、2.の方法を採用することにします。

### 1. RailsのForm機能を利用する
- この方法で実装する場合は、ほとんどRailsで処理が完結するため、AngularJSを利用するまでもないかと思います。

大まかな処理の流れは以下 :

- Viewのレンダリング : **(Rails)** : ```form_for```や```f.text_field```などを用いて記述
- リクエスト処理 : **(Rails)** : ```remote:true```によって、```form_for```のSubmitボタンをAjax化
- 入力フォームのバリデーション: **(Rails)** : Controllerで、バリデーションを実施し、結果をjsonで返す
- CRUD処理 : **(Rails)** : Controllerで、CRUD
- レスポンス処理(リダイレクト、エラーハンドリングなど) : **(Rails + JavaScript)** : ```<<アクション名>>.js.erb```ファイルもしくは、```xxxx.js```ファイルを切り出してレスポンスの処理を記述する(*この場合は、ほとんどjQueryとかで記述するのが通常だと思われます。)

[参考]

[Rails 4 submit modal form via AJAX and render JS response as table row - Eric London's Blog](http://ericlondon.com/2014/03/13/rails-4-submit-modal-form-via-ajax-and-render-js-response-as-table-row.html)

### 2. RailsはAPIに徹する
Viewの処理は、AngularJSにまかせて、RailsはAPIとしての機能に徹します
大まかな処理の流れは以下 : 

- Viewのレンダリング : **(Rails+AngularJS)** : ルーティングして、indexアクションから```index.erb.html```をレンダリングするのは、Rails。```index.erb.html```のデータを組み立てるのは、AngularJS(*```index.erb.html```にRubyのコードは書かない)
- 入力フォームのバリデーション: **(AngularJS/Rails)** : 
 	- AngularJSのバリデーション機能を用いて実装する 
	- RailsのControllerでのバリデーション結果をjsonで返した結果を表示する
	- (サンプルは両方実装しています。この辺どう実装するのがベストプラクティスなんでしょうか。。。)
- リクエスト処理 : **(AngularJS)** : ```ng-click='Create();'```などによって、XHRリクエストを送る
- CRUD処理 : **(Rails)** : Controllerで、CRUD
- レスポンス処理(リダイレクト、エラーハンドリングなど) : **(AngularJS)** : AngularJSのControllerにて、jsonレスポンスから、リダイレクトやエラーハンドリングの処理を記述

## AngularJS+RailsでのCRUDの実装方法
- **2.RailsはAPIに徹する**で実装した場合の、処理を簡単にまとめます
- ソースは、こちらで公開しております
[jwako/angular_crud_app](https://github.com/jwako/angular_crud_app)

### Read

#### indexページの表示
- Rails側では、ルーティングして、```index```アクションから```index.erb.html```をレンダリングする
- ```index.erb.html```に表示する```@items```のデータは、AngularJSから```list```アクションを経由して取得する

```app/controllers/items_controller.rb
class ItemsController < ApplicationController
  before_action :set_item, only: [:show, :detail, :update, :destroy]

  def index
  end

  def list
    @items = Item.all
    render json: @items
  end
```

- AngularJSのItemsCtrlの初期処理にRailsの```list```アクションを呼ぶ処理を記述

```app/assets/javascripts/controllers/crud_controllers.js
var crudControllers = angular.module('crudControllers', ['ui.bootstrap']);

crudControllers.controller('ItemsCtrl', ['$scope', '$http', '$window', '$modal', function ($scope, $http, $window, $modal) {
    $http.get('/items/list').success(function(data) {
      $scope.items = data;
    });

...
```

- ```index.html.erb```では、```ng-repeat="item in items"```でまわしながら、itemを表示していく
- *Rubyのコードは不要

```app/views/items/index.html.erb
<div class="row">
  <div class="col-xs-12">
    <h3>Listing items</h3>

    <div class="pull-right">
      <button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myItemContent">New item</button>
    </div>

    <table class="table" ng-controller="ItemsCtrl">
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
          <th>Description</th>
          <th colspan="3"></th>
        </tr>
      </thead>
      <tbody>
        <tr ng-repeat="item in items">
          <td>{{item.name}}</td>
          <td>{{item.price}}</td>
          <td>{{item.description}}</td>
          <td><a href="/items/{{item.id}}" class="btn btn-info">Show</a></td>
          <td><button class="btn btn-success" ng-click="OpenModal(item)">Edit</button></td>
          <td><button ng-click="Delete(item);" class="btn btn-danger">Delete</button></td>
        </tr>
      </tbody>
    </table>
  </div>
</div>
```

#### モーダル画面の表示
- bootstrapのモーダル画面機能を使って、モーダル画面のHTMLを記述
- Formは、```ng-model```でAngularJSのデータバインディングを用いる
- バリデーションも、AngularJSのバリデーション機能を使う

##### Newの場合のモーダル画面
**モーダル画面表示ボタン**

```app/views/items/index.html.erb
<div class="pull-right">
	<button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myItemContent">New item</button>
</div>
```

**モーダル画面**

```app/views/items/index.html.erb
<div class="modal modal-flex fade" id="myItemContent" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
  <div class="modal-dialog" ng-controller="ItemNewCtrl">
    <div class="modal-content">
    <form name="item">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
        <h4 class="modal-title" id="myModalLabel">New item</h4>
      </div>
      <div class="modal-body">
        <div class="alert alert-danger" ng-show="errors">
          <ul class="list-unstyled" ng-repeat="error in errors">
            <li>{{error}}</li>
          </ul>
        </div>
        <div class="form-group" ng-class="{'has-error': item.name.$invalid}">
          <input type="text" class="form-control" ng-model="name" name="name" placeholder="Name" required autofocus>
          <label class="control-label" for="inputError2" ng-show="item.name.$error.required">*Required</label>
        </div>
        <div class="form-group" ng-class="{'has-error': item.description.$invalid}">
          <textarea class="form-control" ng-model="description" name="description" placeholder="Description" rows="5" required></textarea>
          <label class="control-label" for="inputError2" ng-show="item.description.$error.required">*Required</label>
        </div>
        <div class="form-group" ng-class="{'has-error': item.price.$invalid}">
          <input type="text" class="form-control" ng-model="price" name="price" required ng-pattern="/^([1-9]\d*|0)(\.\d+)?$/">
          <label class="control-label" for="inputError2" ng-show="item.price.$error.required">*Required</label>
          <label class="control-label" for="inputError2" ng-show="item.price.$error.pattern">This is not a valid dollar.</label>
        </div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-success btn-lg pull-right" ng-show="!item.$invalid" 
          ng-click='Create();' ng-disabled="item.$invalid">Submit
        </button>
        <button class="btn btn-success btn-lg pull-right" ng-show="item.$invalid" ng-disabled="item.$invalid">
          Submit
        </button>
      </div>
    </form>
    </div>
  </div>
</div>
```

##### Editの場合のモーダル画面
- この場合、どう記述するのが良いのかぶっちゃけ分かってません。ぜひ、アドバイス/コメントいただければと思います。
- Editの場合は、初期データを取得する必要があるので、```detail```アクションを用意する

```app/controllers/items_controller.rb
class ItemsController < ApplicationController
...
  def detail
    render json: @item
  end
```

- ```OpenModal()```で、```detail```アクションを呼び出し、結果をテンプレートに呼び出す
- この辺りは、Angular BootstrapのModalを参考にした

[Angular directives for Bootstrap](http://angular-ui.github.io/bootstrap/#/modal)

```app/assets/javascripts/controllers/crud_controllers.js
crudControllers.controller('ItemsCtrl', ['$scope', '$http', '$window', '$modal', function ($scope, $http, $window, $modal) {
    $http.get('/items/list').success(function(data) {
      $scope.items = data;
    });

    $scope.OpenModal = function(item) {
      $http.get('/items/' + item.id + '/detail.json').success(function(data) {
        $scope.item = data;
        var modalInstance = $modal.open({
          templateUrl: 'myModalContent.html',
          controller: ItemEditCtrl,
          resolve: {
            item: function () {
              return $scope.item;
            }
          }
        });
      }).error(function(data, status) {
        console.log('error:' + status);
      });
    }
}]);
```

**Editの場合のモーダル画面表示ボタン**

```
<td><button class="btn btn-success" ng-click="OpenModal(item)">Edit</button></td>
```

**Editの場合のモーダル画面**

```app/views/items/index.html.erb
<script type="text/ng-template" id="myModalContent.html">
<div class="modal-header">
  <h4 class="modal-title">Edit item</h4>
</div>
<div class="modal-body">
  <div class="alert alert-danger" ng-show="errors">
    <ul class="list-unstyled" ng-repeat="error in errors">
      <li>{{error}}</li>
    </ul>
  </div>
  <div class="form-group">
    <input type="text" class="form-control" ng-model="item.name" name="name" placeholder="Name" autofocus>
  </div>
  <div class="form-group">
    <textarea class="form-control" ng-model="item.description" name="description" placeholder="Description" rows="5"></textarea>
  </div>
  <div class="form-group">
    <input type="text" class="form-control" ng-model="item.price" name="price">
  </div>
</div>
<div class="modal-footer">
  <button class="btn btn-success btn-lg pull-right" ng-click='Update();'>Update</button>
</div>
</script>
```

### Create

- AngularJSのControllerは、POSTリクエストを出し、```success```の場合はリダイレクト、```error```の場合はエラーハンドリングを記述

```app/assets/javascripts/controllers/crud_controllers.js
crudControllers.controller('ItemNewCtrl', ['$scope', '$http', '$window', function($scope, $http, $window) {
  $scope.Create = function() {
    $http.post('/items.json', {
    	'name': $scope.name,
    	'description': $scope.description,
    	'price': $scope.price
    }).success(function(data, status, headers, config) {
    	if (data.location) {
   			$window.location.href = data.location;
   		}
    }).error(function(data, status) {
      $scope.errors = data;
    });
  }
}]);
```

- Rails側はこんな感じで、```create```メソッドが呼ばれ、jsonを返す

```app/controllers/items_controller.rb
  def create
    @item = Item.new(item_params)

    if @item.save
      render json: {location: item_path(@item)}, status: :created
    else
      render json: @item.errors.full_messages, status: :unprocessable_entity
    end
  end

```

### Update
- Modal画面のControllerとして、```ItemEditCtrl```を登録
- ```Update()```が呼ばれると、PUTリクエストを出し、```success```の場合はリダイレクト、```error```の場合はエラーハンドリングを記述

```app/assets/javascripts/controllers/crud_controllers.js
var ItemEditCtrl = function($scope, $http, $window, $modalInstance, item) {
  $scope.item = item;

  $scope.Update = function(){
    $http.put('/items/' + $scope.item.id, {
      'name': $scope.item.name,
      'description': $scope.item.description,
      'price': $scope.item.price
    }).success(function(data, status, headers, config) {
      if (data.location) {
        $window.location.href = data.location;
      }
    }).error(function(data, status) {
      $scope.errors = data;
    });
  }
}
```

- Rails側はこんな感じで、```update```メソッドが呼ばれ、jsonを返す

```app/controllers/items_controller.rb
  def update
    if @item.update(item_params)
      render json: {location: item_path(@item)}, status: :ok
    else
      render json: @item.errors.full_messages, status: :unprocessable_entity
    end
  end
```

### Delete

- ```Delete()```が呼ばれると、DELETEリクエストを出し、```success```の場合はリダイレクトを記述

```app/assets/javascripts/controllers/crud_controllers.js
    $scope.Delete = function(item){
      $http.delete('/items/' + item.id
      ).success(function(data, status, headers, config) {
        if (data.location) {
          $window.location.href = data.location;
        }
      });
    }
```

- Rails側はこんな感じで、```delete```メソッドが呼ばれ、jsonを返す

```app/controllers/items_controller.rb
  def destroy
    @item.destroy
    render json: {location: items_path}, status: :ok
  end
```

## 注意
- AngularJSからのXHRリクエストの際に```CSRF-TOKEN```を付けて送るため、以下の処理を忘れずに記述しておきます。
- Railsのform_forでは、リクエストパラメータにcsrf-tokenが自動で追加されますが、metaタグでも記述されているcsrf-tokenをリクエストヘッダに手動で追加してやります。

```app/assets/javascripts/angular_app.js
crudApp.config(
	["$httpProvider", function($httpProvider) {
      $httpProvider.defaults.headers.common['X-CSRF-Token'] = $('meta[name=csrf-token]').attr('content');
	  }
	]
);
```

## まとめ
- モーダル画面を使ってCRUDするアプリケーションをAngularJS+Railsで作成する方法についてまとめました。
- ある程度、AngularJSとRailsの役割分担について、RailsをAPIに徹するというひとつの方向性は見出したものの、どうするのが最適かについては分かっておりません。ぜひとも、アドバイスいただければ幸いです。
- Railsアプリケーションの場合、そもそもクライアントサイドをどう管理するのが良いのかについてもご意見いただければうれしいです。

## Ref.

[SrcHndWng/AngularjsWhiskyList](https://github.com/SrcHndWng/AngularjsWhiskyList)

[AngularJSとRuby on Railsで作るCRUDアプリ – (1)環境構築 ｜ Developers.IO](http://dev.classmethod.jp/client-side/javascript/angularjs%E3%81%A8ruby-on-rails%E3%81%A7%E4%BD%9C%E3%82%8Bcrud%E3%82%A2%E3%83%97%E3%83%AA-1%E7%92%B0%E5%A2%83%E6%A7%8B%E7%AF%89/)

[Rails4.0でangularjsを使ってRESTfulなajaxを実装する - is Neet](http://soplana.hateblo.jp/entry/2014/03/01/231008)

[Bootstrapping an AngularJS app in Rails 4.0 - Part 5 :: Adam Anderson](http://asanderson.org/posts/2013/11/20/bootstrap-angular-rails-part-5.html)

[Beryllium Work: Best Practice of Using Angular.js with Rails: Form](http://blog.berylliumwork.com/2013/03/best-practice-of-using-angularjs-with_27.html?m=1)

[Rails 4 submit modal form via AJAX and render JS response as table row - Eric London's Blog](http://ericlondon.com/2014/03/13/rails-4-submit-modal-form-via-ajax-and-render-js-response-as-table-row.html)

[Create New Records with Bootstrap Modal and Rails Unobtrusive Javascript - Flaviu Simihaian's Blog - Entrepreneur and Developer](http://flaviusim.com/blog/2012/05/28/create-new-records-with-bootstrap-modal-and-unobtrusive-javascript-in-rails/)