Ajax でも Rails の flash メッセージを表示したい！

## 概要

通常 Rails の [flash](http://api.rubyonrails.org/classes/ActionController/Flash.html) メッセージは、アクションごとに HTML をレンダリングする場合にしか使用できません。
Rails 側の各アクションで flash メッセージの設定を行う必要があるからです。
つまり、Ajax 通信が終了 (complete) した際に flash メッセージを表示する手段は、デフォルトでは用意されていません。
一応、サーバ側で flash メッセージを表示するための JavaScript を用意して、それをレスポンスとして返すなどの方法はあります。

それでも Ajax 通信 (JSON リクエスト) でも flash メッセージを表示したいよぉ！
というわけでその方法を調べてみました。

## 方法

Rails 側ではレスポンスヘッダ ('X-' で始まるカスタムヘッダ) に flash メッセージを格納してレスポンスを返します。
そして、JavaScript 側ではそれを取り出すことで実現できました。

``` /app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  after_action :flash_to_headers

  private

  def flash_to_headers
    return unless request.xhr?

    response.headers['X-Flash-Messages'] = flash_json

    # ページをリロードした際に flash メッセージが残ってしまうのを防ぐ。 
    flash.discard
  end

  def flash_json
    flash.inject({}) do |hash, (type, message)|
      # XSS 対策を施す。
      message = "#{ERB::Util.html_escape(message)}"
      # 日本語のメッセージをレスポンスヘッダに含めるために URL エンコードしておく。
      message = URI.escape(message)
      hash[type] = message
      hash
    end.to_json
  end
end
```

``` /app/controllers/magicas_controller.rb
class MagicasController < ApplicationController
  respond_to :json, only: [:update]

  def update
    @magica = Magica.find(params[:id])
    @magica.update!(update_params)

    flash[:notice]  = '更新しました！'
    flash[:warning] = '更新しました？'
    flash[:error]   = '更新できず…orz'

    respond_with @magica
  end
end
```

``` /app/views/layouts/application.html.slim
doctype html
html lang="ja"
  head
    meta name="viewport" content="width=device-width, initial-scale=1.0"
    title Magica System
    - description_content = content_for?(:description) ? yield(:description) : "Bootstrap App"
    meta name="description" content=description_content
  
  = stylesheet_link_tag    "application", media: 'all', 'data-turbolinks-track' => true
  = javascript_include_tag "application", 'data-turbolinks-track' => true
  = csrf_meta_tags
  = yield(:head)

  body class=body_class
    = render 'layouts/navigation'
    .js-main-content.container
      / ここに flash メッセージを表示する。
      .js-flash 
        = render 'layouts/messages'
      = yield
```


``` /app/assets/javascripts/flash.js.coffee
# Underscore.js (あるいは Lo-Dash) を導入していることを前提とする。
$(document).ajaxComplete (event, xhr, settings) ->
  flash = $.parseJSON(xhr.getResponseHeader('X-Flash-Messages'))
  return if _.isEmpty(flash)

  $flash = $('.js-flash')
  $flash.empty()

  index = 1
  _.each flash, (message, type) ->

    alertType =
      switch type
        when 'notice'
          'success'
        when 'warning'
          'warning'
        when 'error'
          'danger'
        else
          'info'

    alertId = "js-alert-#{index}"
    decodedMessage = decodeURIComponent(message)
  
    $flash.append """
      <div class='alert alert-#{alertType}' data-alert-id='#{alertId}'>
        <a class='close' data-target='[data-alert-id=#{alertId}]' data-dismiss='alert'>&times;</a>
        <div>#{decodedMessage}</div>
      </i>
    """
    
    index++
  
  return
```

## 実行例

![magica_system.png](https://qiita-image-store.s3.amazonaws.com/0/6697/a6754918-3822-3fbd-36bf-58b1acc42e28.png)

でた！ ✌ ('ω' ✌ )三 ✌ ('ω') ✌ 三( ✌ 'ω') ✌

## 参考

* [How do you handle Rail's flash with Ajax requests?](http://stackoverflow.com/questions/366311/how-do-you-handle-rails-flash-with-ajax-requests)