nested_form + bootstrapでネストしたモデルの入力フォームをタブ表示する

# 目標

以下のように、ネストした入力フォームをタブ表示したい。

![1.png](https://qiita-image-store.s3.amazonaws.com/0/172314/8914d558-67df-41e7-3860-7b7fbe925ad1.png)

# 環境

## 使用gem

+ [rails-bootstrap-form](https://github.com/bootstrap-ruby/rails-bootstrap-forms)
    + railsで簡単にbootstrapのフォームデザインを使用するために使用
+ [nested_form](https://github.com/ryanb/nested_form)
    + 動的にネストしたモデルのためのフォームを追加するために使用

今回の記事に関係のあるgemだけを列挙。
特に直接的に関係のあるものは[nested_form](https://github.com/ryanb/nested_form)。

## その他注意事項

今回の記事のコード例は`turbolinks`に対応していないのでご注意されたい。

# フォームテンプレートファイルを用意する

以下のような形式でテンプレートファイルを作る。

+ bootstrapのタブのパネル表示の文法に従い、`<ul>`と`<div>`の`class`を設定する。
+ CoffeeScriptでタブ追加ボタンをクリックした際の挙動を設定するため、それぞれに任意の`id`も設定しておく。
+ `<ul>`の中身は後にCoffeeScriptで設定する。
+ `f.link_to_add`の出力結果は使用しないが、追加する処理は[nested_form](https://github.com/ryanb/nested_form)を使用したいため、呼び出しだけはしておく。

```_form.html.erb
<%= bootstrap_nested_form_for(@model_a, layout: :horizontal, label_col: "col-xs-2", control_col: "col-xs-10") do |f| %>
  <%= f.text_field :column_a, label: "カラムA" %>
  <%= f.text_field :column_b, label: "カラムB" %>
  .
  .
  .
  <ul class="nav nav-tabs" role="tablist" id="model-b-tab-list">
  </ul>

  <div class="tab-content" id="model-b-tab-contents">
    <%= f.fields_for :nested_model_tables, wrapper: false do |nf| %>
      <div class="tab-pane fields" role="tabpanel">
        <%= nf.text_field :column_a, label: "カラムA" %>
        <%= nf.text_field :column_b, label: "カラムB" %>
        .
        .
        .
      </div>
      <!-- link_to_addを呼び出したいだけで、出力結果は使用しないので、<%= %>ではなくて<% %>で囲む。 -->
      <% f.link_to_add "タブ追加", :part_number_sequences, data: { target: "#model-b-tab-contents" } %>
    <% end %>
  </div>
<% end %>
```

# CoffeeScriptで[nested_form](https://github.com/ryanb/nested_form)に足りない機能を補完する

```model_a.coffee
$ ->
  $(document).on 'ready', ->
    nested_form_tab('model-b-tab-list', 'model-b-tab-contents', 'タブ', '<i class="glyphicon glyphicon-plus"></i>', 'nested_model_tables')
    $("#model-b-tab-contents").children('div').first().addClass('active')

  $(document).on 'nested:fieldAdded:nested_model_tables', (event) ->
    nested_form_tab('model-b-tab-list', 'model-b-tab-contents', 'タブ', '<i class="glyphicon glyphicon-plus"></i>', 'nested_model_tables')
    $("#model-b-tab-contents").children('div').removeClass('active')
    event.field.addClass('active')

  nested_form_tab = (target_ul_id, target_div_id, tab_name_prefix, add_tab_name, assoc) ->
    $("##{target_ul_id}").empty()
    for value, index in $("##{target_div_id}").children('div')
      $(value).attr('id', "#{target_div_id}-#{index + 1}")
      $("##{target_ul_id}").append($("<li role='presentation'><a aria-controls='#{target_div_id}-#{index + 1}' href='##{target_div_id}-#{index + 1}' data-toggle='tab' role='tab'>#{tab_name_prefix}#{index + 1}</a></li>"))
    $("##{target_ul_id}").append($("<li role='presentation'><a data-target='##{target_div_id}' class='add_nested_fields' data-association='#{assoc}' data-blueprint-id='#{assoc}_fields_blueprint' href='javascript:void(0)'>#{add_tab_name}</a></li>"))
```

ページが読み込まれた際とタブ追加ボタンが押された際に、

+ 存在するフォームの数だけ、タブを追加する
+ タブ追加ボタンを追加する
+ ページ読み込み時には一個目のフォームに、フォーム追加時は追加したフォームに`addClass('active')`を実行する

# まとめ

以上の作業を行うと、ネストしたモデルの入力フォームをタブ表示できるようになる。
ページ読み込み時にはタブ１がアクティブになり、タブ追加時には追加したタブがアクティブになる。