RailsでAjax - モーダル表示でフォーム入力。バリデーションエラーが発生したら画面遷移せずにメッセージを表示し、成功時はフラッシュメッセージ付きでリダイレクトしたいとき


## やりたいこと

ちょっとした入力をモーダル上でやりたい。
入力内容に不備があって、登録出来ない場合、画面遷移せずにそのままモーダル上でエラーを表示したい。
入力成功したら、メッセージと共にリダイレクトして画面を再描画したい。

## 前提

- simple_form
- twitter bootstrap
- haml
- device

## 処理の流れ

- View : formからajax呼び出す
- Controller : ajax呼び出しを受け取るコントローラー、レスポンスの返し方
- View : サーバーからのレスポンスをajaxで受け取って、画面を更新

## サンプル実装する処理の概要

仮想のオークションサイトに入札する処理を想定。
以下の要件を満たしたい：

- オークションには最低入札価格が指定されている
- 入札者はオークション詳細からモーダル上で入札金額を入力し、入札する
- 入札金額がオークションの最低入札価格を下回っている場合、入札は受け付けられない

### Model

``` app/models/auction.rb
class Auction < ActiveRecord::Base
  belongs_to :user
  has_many :bids
  
  validate_presence_of :user_id, :name, :min_price
end
```

``` app/models/bid.rb
class Bid < ActiveRecord::Base
  belongs_to :user
  belongs_to :auction
  
  validate_presence_of :user_id, :price, :auction_id
  
  before_save :acceptable_price?

  def acceptable_price?
    if self.auction.min_price < self.price
      true
    else
      self.errors.add(:price, '入札金額が低すぎます。')
      false
    end
  end
end
```

#### POINT

入札を表すBidモデルに、保存前のコールバックを指定している。
親オークションの最低入札価格より入札価格が下回っている場合は、保存させないようにしている。

### View : formからajax呼び出す

``` app/views/bids/new.html.haml
= render 'form'
```


``` app/views/bids/_form.html.haml
= simple_form_for [ @auction, @bid ], remote: true do
  = f.input :price, label: '入札金額'
  = f.button :submit, class: 'btn btn-primary'
```

#### POINT

- formのレンダリング時に ``` remote: true ``` を加える。


### Controller : ajax呼び出しを受け取るコントローラー、レスポンスの返し方

``` remote: true ``` でリクエストが飛んでくるので、受け取るコントローラー側では ``` format.js ``` が呼び出される。

``` format.html ``` はデフォルトなので一応用意してみた。
htmlの時は成功・失敗ともメッセージと共にリダイレクトするのみ。

``` app/controllers/bids_controller.rb
class BidsController < ApplicationController
  before_action :authenticate_user!
  # ...
  
  def create
    @bid = current_user.bids.new(bid_params)
    @bid.auction = @auction
    respond_to do |format|
      if @bid.save
        flash[:notice] = '入札しました。'
        format.html { redirect_to @auction }
        format.js { render js: "window.location = '#{auction_path(@auction)}'" }
      else
        @bid.errors.each do |name, msg|
          flash.now[name] = msg
        end
        format.html { redirect_to @auction }
        format.js { render partial: "shared/message", status: :unprocessable_entity }
      end
    end
  end
  
  # ...
end
```
#### POINT

##### 成功時の処理

- ``` render js: ``` で続く文字列を JavaScript としてブラウザに返す
- リダイレクトしたいパスを ``` window.location ``` にセットすると、レスポンスを受け取った時点でブラウザがJavaScriptコードを実行してくれる

参考URL : [レイアウトとレンダリング | Rails ガイド | Vanilla JavaScriptを出力する](http://railsguides.jp/layouts_and_rendering.html#vanilla-javascript%E3%82%92%E5%87%BA%E5%8A%9B%E3%81%99%E3%82%8B)


> ``` render js: "alert('Hello Rails');" ```
> 上のコードは、引数で与えられた文字列をMIMEタイプtext/javascriptでブラウザに送信します。

参考URL : [Ajaxから指定のURLへリダイレクトする【Rails】 | Kntmrkm.new()](http://kntmrkm.ap01.aws.af.cm/ict/rails/ajax%E3%81%8B%E3%82%89%E6%8C%87%E5%AE%9A%E3%81%AEurl%E3%81%B8%E3%83%AA%E3%83%80%E3%82%A4%E3%83%AC%E3%82%AF%E3%83%88%E3%81%99%E3%82%8B%E3%80%90rails%E3%80%91/)


##### 失敗時の処理

- 失敗した時は、リダイレクトしたくない
- 画面上に表示させたいエラーメッセージは、フラッシュメッセージに詰める
- リダイレクトしないので ``` flash.now ``` を使ってメッセージを渡す
- ``` render partial ``` で部分テンプレートをhtmlとしてレスポンスボディに入れて返す

参考URL : [Action Controller の概要 | Rails ガイド | flash.now](http://railsguides.jp/action_controller_overview.html#flash-now)


### View : サーバーからのレスポンスをajaxで受け取って、画面を更新

``` app/assets/bids.coffee
$ ->
  $('#auction_modal').on("ajax:success", (e, data, status, xhr) ->
    # フラッシュメッセージ付きでリダイレクトするだけなので、success時はココでは何もしない
  ).on "ajax:error", (e, xhr, status, error) ->
    message = xhr.responseText
    $('#error-message').html(message)
```

#### POINT

##### 成功時の処理

コメントの通り、レスポンスをJavaScriptコードとして受け取ったら、ブラウザが自動でリダイレクトしてくれるだけなので、自前のコード上では何もしない。

##### 失敗時の処理

上述の``` render partial ``` で部分テンプレートをhtmlとしてレスポンスボディが ```xhr.responseText ``` に入っているので、それをそのままhtmlとして画面上に埋め込む。

### おまけ

エラーメッセージを生成するパーシャル

``` app/views/shared/_message.html.haml
- flash.each do |name, msg|
  - if msg.is_a?(String)
    %div{ class: "alert alert-#{name == "notice" ? "success" : "danger" }"}
      %button.close{ area: { hidden: "true" }, data: { dismiss: "alert" }, type: "button" } &times;
      = content_tag :div, msg, id: "flash_#{name}"
```