Shred IT!!!!

IT全般について試したこと・勉強したことを綴ったり、趣味について語るブログ

Railsで階層化された複数モデルに対応するフォームの作り方【JavaScript/CoffeeScriptによる動的処理追加】

今回はフォームに「追加・削除」ボタンを設置し、それらをJavascript/CoffeeScriptと連携して動的にする実装をする。

前回の静的な実装までは↓から。
jetglass.hatenablog.jp

タスク

  • 追加に関すること
    • ネストされた要素内に追加ボタンを表示
    • 追加ボタンを押下したら画面上に空のフォームが追加される
    • 追加されたフォームに値を入れて新規作成および更新でレコードが追加される
  • 削除に関すること
    • ネストされた要素内に削除ボタンを表示
    • 削除ボタンを押下したら対象のフォームが画面上から削除される
    • 更新では削除対象のレコードが削除される
    • 新規作成では削除したフォームのレコードが登録されない

実装

モデル

フォームから送信されるパラメーター hoge_attributes内の削除対象としたい要素に、 _destroy=trueというを値を含めて処理させると、対象の要素が削除されるという機能がある。

詳細は下記。
ActiveRecord::NestedAttributes::ClassMethods

この便利機能を利用するためにモデルでallow_destroyという定義をしておく必要がある。

class Event < ActiveRecord::Base
  has_many :event_details
  accepts_nested_attributes_for :event_details, allow_destroy: true #追加
end

class EventDetail < ActiveRecord::Base
  ...
  accepts_nested_attributes_for :topics, allow_destroy: true #追加
end

コントローラー

パラメーターのフィルター処理で_destroyを受け取れるようにする。

    def event_params
      params.require(:event).permit(
        :id,
        :name,
        event_details_attributes: [
          :id,
          :detail,
          :_destroy, #追加
          topics_attributes: [:id, :name, :_destroy] #追加
        ]
      )
    end

コントローラーに追加されているこのフィルター処理、strong_parameters と呼ばれているみたい。 github.com

ヘルパー

追加ボタンと削除ボタンを設置する際のヘルパーを準備する。

module EventsHelper
  # 追加ボタン
  def link_to_add_field(name, f, association, options={})
    # association で渡されたシンボルから、対象のモデルを作る
    # 前回コントローラーで実装したモデルの build にあたる処理
    new_object = f.object.class.reflect_on_association(association).klass.new

    # Javascript 側で配列のインデックス値とする
    # 追加しまくると、インデックス値がかぶりまくるので、
    # 後に Javascript 側でこのインデックス値は現在時刻をミリ秒にした値で置き換えていく
    id = new_object.object_id

    # f はビューから渡されたフォームオブジェクト
    # fields_for で f の子要素を作る
    fields = f.fields_for(association, new_object, child_index: id) do |builder|
      render(association.to_s.singularize + "_form", f: builder)
    end

    # ボタンの設置。classを指定してJavascriptと連動、fields を渡しておいて、
    # ボタン押下時にこの要素(fields)をJavascript側で増やすようにする
    link_to(name, '#', class: "add_field", data: {id: id, fields: fields.gsub("\n","")})

    # Rails3系だと下記のように書けるが、4系で link_to_function は葬られた
    #link_to_function(name, raw("add_field(this, \"#{association}\", \"#{escape_javascript(fields)}\")"), options)
  end

  #削除ボタン
  def link_to_remove_field(name, f, options={})
    # _destroy の hiddenフィールドと削除ボタンを設置
    f.hidden_field(:_destroy) + link_to(name, '#', class: "remove_field")
  end
end

ビュー

要素を追加するボタンと削除するボタンをヘルパーを使って設置。

#_form.html.erb 一部抜粋
  <div class="field">
    ...
    <%= f.fields_for :event_details do |df| %>
      <%= render partial: "event_detail_form", locals: {f: df } %>
    <% end %>
    <%= link_to_add_field("detail add", f, :event_details, {}) #追加 %>
  </div>

#_event_detail.html.erb 一部抜粋
<div class="field">
  ...
  <%= f.fields_for :topics do |tf| %>
    <%= render partial: "topic_form", locals: {f: tf } %>
  <% end %>
  <%= link_to_add_field("topic add", f, :topics, {}) #追加 %>
  <br>
  <%= link_to_remove_field("detail remove", f, {}) #追加 %>
</div>

#_topic_form.html.erb 一部抜粋
<div class="field">
  ...
  <%= f.hidden_field :id %>
  <br>
  <%= link_to_remove_field("topic remove", f, {}) #追加 %>
</div>
# coffeescript
# Rails4では Turbolinks が動作していて、
# この書き方でないと ready イベントが発火しない
$(document).on 'ready page:load', ->
  # 追加ボタンを押されたとき
  $('form').on 'click', '.add_field', (event) ->
    # 現在時刻をミリ秒形式で取得
    time = new Date().getTime()
    # ヘルパーで作ったインデックス値を↑と置換
    regexp = new RegExp($(this).data('id'), 'g')
    # ヘルパーから渡した fields(HTML) を挿入
    $(this).before($(this).data('fields').replace(regexp, time))
    event.preventDefault()

  # 削除ボタンを押されたとき
  $('form').on 'click', '.remove_field', (event) ->
    # 削除ボタンを押したフィールドの _destroy = true にする
    $(this).prev('input[name*=_destroy]').val('true')
    # 削除ボタンが押されたフィールドを隠す
    $(this).closest('div').hide()
    event.preventDefault()

CSS

画面表示を見やすくするためにフィールドや要素を枠で囲む

.field {
  border: 1px solid #000;
  padding: 10px;
  margin: 10px;
}

動作確認

わかりづらいが、gif 動画を残してみる。

f:id:jetglass:20150417185629g:plain

追加ボタン(ボタンっていうかリンク)を押すと、対象のフィールドが追加され、 削除ボタンを押すと、対象のフィールドが削除される。

上記の動画では確認できないが、追加・削除したフォームの状態はそっくりそのまま保存される。

パラメーターは下記の通り送信される。

[1] pry(#<EventsController>)> event_params
=> {
"id"=>"3",
"name"=>"ab",
"event_details_attributes"=>
  # "0" の中身は登録済みデータを削除ボタンで削除した場合
  {"0"=>
    {"id"=>"3",
     "detail"=>"ab",
     "_destroy"=>"true", # 削除ボタンによりtrue
     "topics_attributes"=>
       {"0"=>
         {"id"=>"3", "name"=>"ab", "_destroy"=>"true" # 削除ボタンによりtrue}
       }
     },
     # 追加ボタンで追加した場合、キーが現在日時のミリ秒
     "1429265236985"=>
      {"id"=>"", # 追加なのでidは空
       "detail"=>"cd",
       "_destroy"=>"false", # 追加なので _destroy はfalse
       "topics_attributes"=>
         {"1429265242729"=>
            {"id"=>"", "name"=>"ef", "_destroy"=>"false" # 追加なので id は空、 _destroy はfalse},
           # ボタン押下の現在日時をミリ秒にしているので、キーの数字が↑と異なる
          "1429265245905"=>
            {"id"=>"", "name"=>"gh", "_destroy"=>"false" # 追加なので id は空、 _destroy はfalse}
}}}}

これら event_params は 新規作成でも更新でもよしなに処理される。

参考

ruby-rails.hatenadiary.com