LoginSignup
94
100

More than 5 years have passed since last update.

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

Posted at

やりたいこと

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

前提

  • 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を出力する

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

参考URL : Ajaxから指定のURLへリダイレクトする【Rails】 | Kntmrkm.new()

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

参考URL : Action Controller の概要 | Rails ガイド | 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}"
94
100
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
94
100