Rails 複数の子レコードの作成・更新を自在に扱う (accepts_nested_attributes_for)

# accepts_nested_attributes_for
ひとつのフォームの中で、associationで関連づいた子レコードまで保存させたいときにaccepts_nested_attributes_forは非常に便利です。

以前、以下のようにまとめました。
[Rails ネストした関連先のテーブルもまとめて保存する (accepts_nested_attributes_for、fields_for)](http://qiita.com/shizuma/items/6f56ca442111ece021b5)

ただし、子レコードの更新(削除)や、子レコードの数の変更まで行いたかったのでやり方をまとめておきます。（メモ程度なので詳しくは説明書きませんでしたが、参考になれば。）

以下の解説でnew(create)とedit(update)のみにフォーカスして説明します。
簡単なCRUDを作ったrepositoryを用意してありますので、もし詳細が気になった方は参照されて下さい。
https://github.com/kuboshizuma/accepts_attributes_team/
＊Viewは適当で、大して綺麗にしてないですのでご了承を。

# 実装

## Model

以下のModelのように一つの「team」に複数の「member」が属する形です。子レコードは「member」のレコードにあたります。```teams#new```でteamを新規登録するときにmemberまで一緒に登録させます。

```rb:member.rb
class Member < ActiveRecord::Base
  belongs_to :team
end
```

```rb:team.rb
class Team < ActiveRecord::Base
  has_many :members
  accepts_nested_attributes_for :members, allow_destroy: true
end
```


```accepts_nested_attributes_for :members, allow_destroy: true```の部分はaccepts_nested_attributes_forで登録する子Modelを指定します。```allow_destroy```というオプションがポイントだったりするのですが、このオプションのおかげでeditするときに子レコードを削除出来るようになります。

## Controller

次に、コントローラーですが以下見て頂ければ何しているか察してもらえると思います。注意する必要があるのはストロングパラメーターの設定の箇所で

- attributesを許可する
- updateでは_destoryとidを含める

ということぐらい注意すればオーケーです。

```rb:teams_controller.rb
class TeamsController < ApplicationController
  def new
    @team = Team.new
    @team.members.build
  end

  def create
    Team.create(team_params)
    redirect_to root_path
  end

  def edit
    @team = Team.find(params[:id])
  end

  def update
    @team = Team.find(params[:id])
    if @team.update(update_team_params)
      redirect_to root_path
    else
      render :edit
    end
  end

  private
  def team_params
    params.require(:team).permit(:name, :description, members_attributes: [:name, :grade])
  end

  def update_team_params
    params.require(:team).permit(:name, :description, members_attributes: [:name, :grade, :_destroy, :id])
  end

end
```

## View

そしてViewですが、newもeditも一緒です。下のテンプレートを読み込んで下さい。
子レコードを作成するときはfields_forを使います。また、注意することは```= m.check_box:_destroy```です。これにチェックを入れると子レコードが削除されます。
また、要素を追加するために```%p#add_item_button 追加する```のボタンを用意していますが、この次にjQueryで実装します。

```_form.html.haml
.container.row.form
  %p チームの作成
  .col-lg-8
    = form_for @team do |f|
      = f.text_field :name, placeholder: 'チーム名を入力して下さい', cols: '30', rows: '10', autofocus: 'true', class: 'form-control', required: true
      = f.text_area :description, placeholder: 'チーム紹介をして下さい', autofocus: 'true', class: 'form-control', required: true
      #team_members_box
        %p メンバー
        = f.fields_for :members do |m|
          .js-team_member{ id: "add_member_#{m.index}" }
            = m.text_field :name, placeholder: 'メンバーの名前を入力して下さい', class: 'form-control', required: true
            = m.text_field :grade, placeholder: 'メンバーの学年を入力して下さい', class: 'form-control', required: true
            = m.check_box:_destroy
            %span.member_delete{ data: { id: m.index, default: 'default' } }
              Delete
      %p#add_item_button 追加する
      = f.submit '作成する', class: 'btn btn-primary'
  = link_to '一覧に戻る', root_path
```

## jQuery

ここで、子レコードのための要素の追加・削除が出来るようにしています。「Delete」ボタンを押すと子レコード削除のチェックボックスにチェックが入るようにもしています。チェックボックスはcssで```display: none```で消しておいて下さい。

```js:teams.js
$(function() {
  var member_num = $('.js-team_member').length;
  $('#add_item_button').on('click', function() {
    var input =
        '<div class="js-team_member" id="add_member_' + member_num + '">'
        + '<input class="form-control" placeholder="メンバーの名前を入力して下さい" type="text" name="team[members_attributes][' + member_num + '][name]" id="team_members_attributes_' + member_num + '_name" required>'
        + '<input class="form-control" placeholder="メンバーの年齢を入力して下さい" type="text" name="team[members_attributes][' + member_num + '][grade]" id="team_members_attributes_' + member_num + '_grade" required>'
        + '<span class="member_delete" data-id="' + member_num + '">'
        +   'Delete'
        + '</span>'
        +'</div>'
    $('#team_members_box').append(input);
    member_num ++;
  });

  $('#team_members_box').on('click', '.member_delete', function() {
    var inputId = $(this).data('id');
    var defaultData = $(this).data('default');
    if (defaultData == 'default') {
      $(this).prev().prop('checked', true);
      $('#add_member_' + inputId).hide();
    }else{
      $('#add_member_' + inputId).remove();
    }
  });
});
```