LoginSignup
57
58

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-08-13

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

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

jwako/angular_crud_app

AngularJS(フロントサイド)とRailsの役割分担と実装パターン

  • まず、モーダル画面を使って(AngularJSに限らずとも)、RailsでCRUDアプリケーションを作成する場合、フロントサイドとサーバーサイドで、誰が何をどこまでやるかを決めないといけません。
  • <何を>を分解すると以下のようになります。

    • Viewのレンダリング
    • 入力フォームのバリデーション
    • リクエスト処理
    • CRUD処理
    • レスポンス処理(リダイレクト、エラーハンドリングなど)
  • いくつか実装方法はあると思いますが、Railsの役割に注目すると、次の大きく次の2つのやり方があると思います。

  • 本エントリでは、2.の方法を採用することにします。

1. RailsのForm機能を利用する

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

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

  • Viewのレンダリング : (Rails) : form_forf.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

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

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

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

AngularJSとRuby on Railsで作るCRUDアプリ – (1)環境構築 | Developers.IO

Rails4.0でangularjsを使ってRESTfulなajaxを実装する - is Neet

Bootstrapping an AngularJS app in Rails 4.0 - Part 5 :: Adam Anderson

Beryllium Work: Best Practice of Using Angular.js with Rails: Form

Rails 4 submit modal form via AJAX and render JS response as table row - Eric London's Blog

Create New Records with Bootstrap Modal and Rails Unobtrusive Javascript - Flaviu Simihaian's Blog - Entrepreneur and Developer

57
58
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
58