RailsでActiveRecordを使わない1対多のフォームとバリデーション

## はじめに
RailsでActiveRecordを使わずに1対多の親子関係を持った入れ子のフォームを作成します。
ActiveModelを使うことで、ActiveRecordの`has_many`を使う感覚で、1対多のmodelを構成します。

以前書いた、[ActiveRecordを使わない動的なフォームとバリデーション](https://qiita.com/TakeshiFukushima/items/fdb652408da1353fbbf9) の改良バージョンになります。

GitHubで作ったものを公開しています。
https://github.com/FukushimaTakeshi/nested_form2

### 環境
Ruby : 2.4.2
Ruby on Rails : 5.1.4

## 作ったもの
scaffoldベースの簡単な問い合わせフォームです。↓のイメージの通り、1対多の関連を持つフォームです。

#### 初期表示
<img width="679" alt="2018-01-17_00h22_24.png" src="https://qiita-image-store.s3.amazonaws.com/0/139933/4c081cda-7bde-12eb-c954-e8f9da00c33b.png">


#### バリデーションエラー時
子モデルのバリデーションエラーを特定し、エラーがあった入力エリアを赤くします。
<img width="632" alt="2018-01-17_00h23_39.png" src="https://qiita-image-store.s3.amazonaws.com/0/139933/df98f38e-0fce-f7c6-7be8-b82682aa447a.png">



#### 子モデルのデータ件数
この場合、3列分の子モデルを画面に表示できるようにします。

```sqlite3
sqlite> select count(*) from free_forms;
3
```
列数を動的に出力したかっただけで、このテーブルに意味はないです。

## Model
`ActiveModel::Model`をincludeすることでActiveRecordと同じような感じでバリデーションが定義できます。

```ruby:［親のモデル］models/inquiry.rb
class Inquiry
  include ActiveModel::Model

  attr_accessor :name, :tel, :email, :inquiry_details

  validates :name, presence: true, length: { maximum: 20 }
  validates :tel, presence: true, numericality: true, length: { maximum: 15 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX }

  def valid?
    valid_inquiry_details = @inquiry_details.map { |v| v.valid? }.all?
    super && valid_inquiry_details
  end

  def save
  end

  def inquiry_details_attributes=(attributes)
    @inquiry_details = attributes.map { |_k, v| InquiryDetail.new(v) }
  end
end
```

##### 親モデルのポイント
- `attr_accessor`で子モデルとなるオブジェクトを定義。この場合`inquiry_details` が子モデル
- `xxx_attributes=` というメソッドを用意する。`xxx`には後述する`fields_for` の引数を指定する。 この場合`inquiry_details`が入り`inquiry_details_attributes=`というメソッド名になる。
- `@inquiry_details.map { |v| v.valid? }`で子モデルのインスタンスをまとめてvalidする。

<br>

```ruby:［子のモデル］models/inquiry_datail.rb
class InquiryDetail
  include ActiveModel::Model
  attr_accessor :detail, :detail2, :detail3, :detail4, :detail5
  validates :detail, length: { maximum: 5 }
  validates :detail2, length: { maximum: 5 }
  validates :detail3, length: { maximum: 5 }
  validates :detail4, length: { maximum: 5 }
  validates :detail5, length: { maximum: 5 }
end
```

## Controller
一応、入力→確認→送信 の問い合わせフォームのつもりですが、newとconfirmのバリデーション部分までで他は仮で作っています。

```ruby:controllers/inquiry_controller.rb
class InquiryController < ApplicationController

  def new
    @free_form = FreeForm.all
    @inquiry = Inquiry.new(inquiry_details: Array.new(@free_form.count).map { InquiryDetail.new })
  end

  def confirm
    @free_form = FreeForm.all
    @inquiry = Inquiry.new(inquiry_params)
    render :new unless @inquiry.valid?
  end

  def create
    @inquiry = Inquiry.new(inquiry_params)
    @inquiry.save
  end

  private
  # Strong Parameters
  def inquiry_params
    params
      .require(:inquiry)
      .permit(
        :name,
        :tel,
        :email,
        inquiry_details_attributes: [
          :detail,
          :detail2,
          :detail3,
          :detail4,
          :detail5
        ]
      )
  end
end
```
##### Controllerのポイント

- 子モデルのオブジェクト(InquiryDetail)を出力する列数分、配列で詰める
 `@inquiry = Inquiry.new(inquiry_details: Array.new(@free_form.count).map { InquiryDetail.new })`

- StrongParameterで子モデルを許可する際に`xxx_attributes`というパラメータを指定する。`xxx`には後述する`fields_for` の引数を指定する。 この場合`inquiry_details`が入り`inquiry_details_attributes`になる。

## View
`form_for`で親モデル `fields_for` で子モデルの入れ子を定義します。`fields_for` のブロックでは子モデルのインスタンス分、繰り返されます。

```erb:views/inquiry/new.html.erb
<%= form_for @inquiry, url: inquiry_confirm_path do |f| %>
  <div>
    <h1>お問い合わせ</h1>
  </div>

  <!-- エラーメッセージの出し方は結構無理やりなので、もっとスマートな書き方があると思います。 -->
  <% if @inquiry.errors.any? || @inquiry.inquiry_details.any? { |inquiry_detail| inquiry_detail.errors.present? } %>
    <div>
      <strong>入力内容にエラーがあります</strong>
      <ul>
        <% @inquiry.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>

        <% @inquiry.inquiry_details.each do |inquiry_detail| %>
          <% inquiry_detail.errors.full_messages.each do |msg| %>
            <li><%= msg %></li>
          <% end %>
        <% end %>
      </ul>
    </div>
  <% end %>

  <table>
    <tr>
      <th><%= f.label :name %></th>
      <td><%= f.text_field :name %></td>
    </tr>
    <tr>
      <th><%= f.label :tel %></th>
      <td><%= f.text_field :tel %></td>
    </tr>
    <tr>
      <th><%= f.label :email %></th>
      <td><%= f.text_field :email %></td>
    </tr>
    <table style="border-top: 2px solid #bbb;">
      <tr><br>
        <td>
        <% @free_form.present? %>
          <tr>
            <th><%= "詳細1" %></th>
            <th><%= "詳細2" %></th>
            <th><%= "詳細3" %></th>
            <th><%= "詳細4" %></th>
            <th><%= "詳細5" %></th>
          </tr>
          <%= f.fields_for :inquiry_details do |inquiry_f| %>
            <tr>
              <td><%= inquiry_f.text_field :detail %></td>
              <td><%= inquiry_f.text_field :detail2 %></td>
              <td><%= inquiry_f.text_field :detail3 %></td>
              <td><%= inquiry_f.text_field :detail4 %></td>
              <td><%= inquiry_f.text_field :detail5 %></td>
            </tr>
          <% end %>
        </td>
      </tr>
    </table>
  </table>
  <%= f.submit '確認' %>
<% end %>
```

##### Viewのポイント
- フォームとModelを紐付けるために親モデルを`form_for` で指定する
- Modelの親子関係のため`form_for`内で `fields_for`を定義する。引数には子モデルのインスタンスを指定する


```
<%= form_for @inquiry, url: inquiry_confirm_path do |f| %>
  ...
  <%= f.fields_for :inquiry_details do |inquiry_f| %>
    ...
```