モーダル画面を使ってCRUDするアプリケーションをAngularJS+Railsで作成する方法を解説します。
本エントリにあたり、SrcHndWng/AngularjsWhiskyListを全面的に参考にさせていただきました。ありがとうございます。
また、本エントリのソースはこちらで公開しております。
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
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
アクションを経由して取得する
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
アクションを呼ぶ処理を記述
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のコードは不要
<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の場合のモーダル画面
モーダル画面表示ボタン
<div class="pull-right">
<button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#myItemContent">New item</button>
</div>
モーダル画面
<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
アクションを用意する
class ItemsController < ApplicationController
...
def detail
render json: @item
end
-
OpenModal()
で、detail
アクションを呼び出し、結果をテンプレートに呼び出す - この辺りは、Angular BootstrapのModalを参考にした
Angular directives for Bootstrap
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の場合のモーダル画面
<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
の場合はエラーハンドリングを記述
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を返す
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
の場合はエラーハンドリングを記述
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を返す
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
の場合はリダイレクトを記述
$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を返す
def destroy
@item.destroy
render json: {location: items_path}, status: :ok
end
注意
- AngularJSからのXHRリクエストの際に
CSRF-TOKEN
を付けて送るため、以下の処理を忘れずに記述しておきます。 - Railsのform_forでは、リクエストパラメータにcsrf-tokenが自動で追加されますが、metaタグでも記述されているcsrf-tokenをリクエストヘッダに手動で追加してやります。
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.
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