Rails + JBuilder + AngularJSでJSONベースの画面処理
処理の流れ
処理のおおまかな流れとしては上記のような感じです。
では、画面イメージとソースコードで説明していきます。
一覧表示
app/controllers/companies_controller.rb
class CompaniesController < ApplicationController
def index
end
def search
@companies = Company.all
respond_to do |format|
format.json
end
end
・・・
indexメソッドはデータなしのHTMLを返却するだけなので、特に何もしていません。
HTMLロード後に非同期で呼び出されるsearchメソッドでデータを全件取得し
インスタンス変数@companiesに保持しています。
app/views/companies/search.json.jbuilder
json.array!(@companies) do |company|
json.(company, :id, :company_name, :boss_name)
json.edit_url companies_edit_path(company.id)
json.emails company.emails do |email|
json.(email, :id, :email)
end
end
出力されるJSONデータ
[{"id": 1,"company_name": "株式会社ラクーン","boss_name": "楽太郎","edit_url": "/companies/1/edit","emails": [{"id": 1,"email": "rakutaro1@raccoon.ne.jp"},{
"id": 2,"email": "rakutaro2@raccoon.ne.jp"}]},{"id": 2,"company_name": "株式会社トラスト&グロース",・・・
コントローラで取得した@companiesからJSONデータを生成しています。
json.(company, :id, :company_name, :boss_name)
は、第1引数の要素のうち、列挙された項目について出力しています。
尚、下記のコードと同義です。
json.extract! company, :id, :company_name, :boss_name
json.emails company.emails do |email|
json.(email, :id, :email)
end
で配列項目を出力しています。
また下記のような、より簡潔な書き方も可能です。
json.emails company.emails, :id, :email
json.edit_url companies_edit_path(company.id)
companies_edit_path()でroutes.rbの定義からcompanies_editのパスを生成します。
・・・
<div class="container" ng-controller="CompanyCtrl as companyCtrl">
<h1>会社一覧</h1>
<table class="table table-bordered">
<thead>
<tr>
<th>会社名</th>
<th>代表者名</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="company in companyCtrl.companies">
<td>{{company.company_name}}</td>
<td>{{company.boss_name}}</td>
<td><a ng-href="{{company.edit_url}}">編集</a></td>
</tr>
</tbody>
</table>
</div>
・・・
(function() {
function CompanyCtrl($http) {
var self = this;
self.companies = [];
$http.get("companies/search.json").then(function(result){
self.companies = result.data;
});
}
angular.module("SampleApp").controller("CompanyCtrl",CompanyCtrl);
})();
$http.get("companies/search.json").then(function(result){
self.companies = result.data;
});
画面に表示しています。
<tr ng-repeat="company in companyCtrl.companies">
<td>{{company.company_name}}</td>
編集画面
先ほどの一覧画面の"編集"リンクを押すとこの画面が開きます。
まずは初期表示から
app/controllers/companies_controller.rb
・・・
class CompaniesController < ApplicationController
・・・
def edit
end
def load
@company = Company.find(params[:id])
respond_to do |format|
format.json
end
end
・・・
一覧画面と同じく、editメソッドはデータなしのHTMLを返却するだけなので、特に何もしていません。
HTMLロード後に非同期で呼び出されるloadメソッドで受け取ったパラメータ"id"に紐づくレコードを1件取得してインスタンス変数@companyに保持しています。
app/views/companies/load.json.jbuilder
json.(@company, :id, :company_name, :boss_name)
json.emails @company.emails do |email|
json.(email,:id,:email)
json._destroy false
end
json.update_path companies_update_path(format: :json)
出力されるJSONデータ
{"id": 1,"company_name": "株式会社ラクーン","boss_name": "楽太郎","update_path": "/companies/1/update.json","emails": [{"_destroy": false,"email": "rakutaro1@raccoon.ne.jp","id": 1},{"_destroy": false,"email": "rakutaro2@raccoon.ne.jp","id": 2}]}
format: :jsonと指定することで、パスが"update.json"と出力されます。
json.update_path companies_update_path(format: :json)
"_destroy"については、この後の更新処理で説明します。
・・・
<div class="container" ng-controller="CompanyEditCtrl as companyCtrl">
<h3>編集</h3>
<form>
<div class="form-group" ng-class="{'has-error': companyCtrl.isError('company_name')}">
<label>会社名</label>
<input type="text" class="form-control" ng-model="companyCtrl.company.company_name">
<span class="text-danger" ng-repeat="message in companyCtrl.getMessages('company_name')">
{{message}}
</span>
</div>
<div class="form-group" ng-class="{'has-error': companyCtrl.isError('boss_name')}">
<label>代表者名</label>
<input type="text" class="form-control" ng-model="companyCtrl.company.boss_name">
<span class="text-danger" ng-repeat="message in companyCtrl.getMessages('boss_name')">
{{message}}
</span>
</div>
<div class="form-group row " ng-repeat="email in companyCtrl.company.emails">
<label class="control-label col-md-12" ng-show="$first">メールアドレス</label>
<div class="col-md-12" ng-class="{'has-error': companyCtrl.isError('emails[{{$index}}].email')}">
<input type="text" class="form-control" ng-model="email.email">
<span class="text-danger" ng-repeat="message in companyCtrl.getMessages('emails['+$index+'].email')">
{{message}}
</span>
</div>
</div>
<input type="button" class="btn btn-primary" value="更新" ng-click="companyCtrl.update()">
</form>
</div>
・・・
(function() {
function CompanyEditCtrl($http,$window,$location) {
var self = this;
self.company = {};
self.errors = [];
$http.get($location.path() + "/load.json").then(function(result){
self.company = result.data;
});
・・・
angular.module("SampleApp").controller("CompanyEditCtrl", CompanyEditCtrl);
})();
一覧画面と同じく、この部分でJSONデータを非同期で取得しています。
$http.get($location.path() + "/load.json").then(function(result){
self.company = result.data;
});
続いて更新処理
app/controllers/companies_controller.rb
・・・
class CompaniesController < ApplicationController
・・・
def update
@company = Company.find(params[:id])
if @company.update(update_params)
render json: { path: companies_path }
else
render :update_error, status: :unprocessable_entity
end
end
private
def update_params
params.require(:company).permit(
:company_name,
:boss_name,
emails_attributes: [ :id, :email, :_destroy ]
)
end
・・・
@company.update(update_params)
で、画面から送信されたパラメータでデータを更新しています。
更新が成功した場合は
render json: { path: companies_path }
で一覧画面のパスを返し、AngularJSのコントローラ側で画面遷移を実行しています。
更新が失敗した場合については後述します。
(function() {
function CompanyEditCtrl($http,$window) {
var self = this;
self.company = {};
self.errors = [];
・・・
self.update = function() {
angular.forEach(self.company.emails,function(v,i) {
v._destroy = v.email === "";
});
$http.put(self.company.update_path, {
company: {
company_name: self.company.company_name,
boss_name: self.company.boss_name,
emails_attributes: self.company.emails
}
}).then(function(result){
$window.location.href = result.data.path;
} ,function(result) {
self.errors = result.data.errors.messages;
});
}
self.isError = function(key) {
for(var i = 0; i < self.errors.length; i++) {
if(self.errors[i].key === key) {
return true;
}
}
return false;
}
self.getMessages = function(key) {
for(var i = 0; i < self.errors.length; i++) {
if(self.errors[i].key === key) {
return self.errors[i].messages;
}
}
return null;
}
}
angular.module("SampleApp").controller("CompanyEditCtrl", CompanyEditCtrl);
})();
app/views/companies/update_error.json.jbuilder
json.errors do
json.messages do
json.array!(@company.errors.to_hash(true)) do |key, messages|
next if key.to_s.include?("emails.")
json.key "#{key}"
json.messages messages
end
@company.emails.each_with_index do |v, i|
json.array!(v.errors.to_hash(true)) do |key, messages|
json.key "emails[#{i}].#{key}"
json.messages messages
end
end
end
end
出力されるJSONデータ
{"errors": {"messages": [{"key": "company_name","messages": ["Company name can't be blank"]}]}}
ng-click="companyCtrl.update()"
$http.put(self.company.update_path, {
company: {
company_name: self.company.company_name,
boss_name: self.company.boss_name,
emails_attributes: self.company.emails
}
})
更新結果が正常の場合、一覧画面に遷移
.then(function(result){
$window.location.href = result.data.path;
}
,function(result) {
self.errors = result.data.errors.messages;
});
<span class="text-danger" ng-repeat="message in companyCtrl.getMessages('company_name')">
{{message}}
</span>
ng-class="{'has-error': companyCtrl.isError('company_name')}"
配列項目のエラー処理について
今回のサンプルではメールアドレスがそれに該当します。
app/views/companies/edit.json.jbuilder
json.errors do
json.messages do
json.array!(@company.errors.to_hash(true)) do |key, messages|
next if key.to_s.include?("emails.")
json.key "#{key}"
json.messages messages
end
@company.emails.each_with_index do |v, i|
json.array!(v.errors.to_hash(true)) do |key, messages|
json.key "emails[#{i}].#{key}"
json.messages messages
end
end
end
end
出力されるJSONデータ
{"errors": {"messages": [{"key": "emails[0].email","messages": ["Email is invalid"]},{"key": "emails[1].email","messages": ["Email is invalid"]}]}}
@company.emails.each_with_index do |v, i|
のループ処理内で
json.key "emails[#{i}].#{key}"
要素番号付きの項目名にマッピングしてエラーメッセージを表示します。
<span class="text-danger" ng-repeat="message in companyCtrl.getMessages('emails['+$index+'].email')">{{message}}</span>
同様に、テキストボックスの背景が赤くなるクラスを付与しています。
ng-class="{'has-error': companyCtrl.isError('emails[{{$index}}].email')}"
ちなみに
next if key.to_s.include?("emails.")
削除処理について
_destroyパラメータを使用した削除処理について説明します。
モデルに_destroy属性をtrueで設定して更新処理を行うと、ActiveRecordはそのレコードを削除してくれるのでそれを利用します。
(function() {
・・・
self.update = function() {
angular.forEach(self.company.emails,function(v,i) {
v._destroy = v.email === "";
});
$http.put(self.company.update_path, {
company: {
company_name: self.company.company_name,
boss_name: self.company.boss_name,
emails_attributes: self.company.emails
}
・・・
■RailsのController
app/controllers/companies_controller.rb
・・・
class CompaniesController < ApplicationController
・・・
def update
@company = Company.find(params[:id])
if @company.update(update_params)
render json: { path: companies_path }
else
render :update_error, status: :unprocessable_entity
end
end
private
def update_params
params.require(:company).permit(
:company_name,
:boss_name,
emails_attributes: [ :id, :email, :_destroy ]
)
end
・・・
メールアドレスが空の場合、_destroyをtrueに設定しサーバへ送信
angular.forEach(self.company.emails,function(v, i) {
v._destroy = v.email === "";
});
受け取ったパラメータでモデルをそのまま更新
@company.update(update_params)
以上、かなり簡潔化した内容ではありましたが、JSONベースの画面処理についてのご紹介でした。