ActiveRecordを使わない動的なフォームとバリデーション

## はじめに
Railsでデータベース(ActiveRecord)を使用しない可変部分を含む動的なFormを作成します。
ActiveModelを使用してFormと紐付かせると、ActiveRecordを使用した場合と同様にValidationを使用することができます。
今回、フォームの一部の項目が可変なパターンが探してもなかったので、そのやり方をまとめておきました。

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

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

## 作ったもの
scaffoldベースの簡単な問い合わせフォームです。このフォームの備考欄をテーブルのデータ数にあわせて、動的に表示します。

#### 初期表示
<img width="400" alt="2017-11-20_00h31_36.png" src="https://qiita-image-store.s3.amazonaws.com/0/139933/adbbe3dd-664d-221d-03b3-1413f6388bfa.png">

#### バリデーションエラー
Railsでは[デフォルト](https://github.com/rails/rails/blob/master/actionview/lib/action_view/base.rb#L145)でバリデーションエラー時にラベルやフォームの要素を`field_with_errors`というclassのdivで囲んでくれるので、cssなどでそのclassを指定しておけばエラーが発生した項目に色を付けられます。
<img width="369" alt="2017-11-20_00h40_24.png" src="https://qiita-image-store.s3.amazonaws.com/0/139933/8007867b-6578-6216-8fe5-acf0e9c73c61.png">


#### 可変部分のデータ
この場合、3件の備考欄を表示できるようにします。

```sqlite3
sqlite> select * from free_forms;
1|備考1|2017-11-19 12:03:06.254618|2017-11-19 12:03:06.254618
2|備考2|2017-11-19 12:03:06.303466|2017-11-19 12:03:06.303466
3|備考3|2017-11-19 12:03:06.393128|2017-11-19 12:03:06.393128
sqlite>
```

## Model
`ActiveModel::Model`をincludeすることでActiveRecordと同じような感じでバリデーションが定義できます。
ポイントはレコード数によって変動する備考欄のところを`define_free_text_method`内で`class_eval `を使用してアクセサとバリデーションを定義しているところです。バリデーションエラーで色を付けるためには[ここ](https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/active_model_helper.rb#L42)を通さないとダメっぽいので、アクセサの定義は必要です。
`I18n.backend.store_translations`ではテーブルの値に基づき新しい翻訳文を追加しています。

```ruby:models/inquiry.rb
class Inquiry
  include ActiveModel::Model

  attr_accessor :name, :tel, :email

  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 initialize(free_form, params={})
    define_free_text_method(free_form)
    super(params)
  end

  def save!
  end

  private

  def define_free_text_method(free_form)
    free_form.each_with_index do |value, index|
      class_eval do
        attr_accessor :"free_text_#{index}"
        validates :"free_text_#{index}", length: { maximum: 100 }
      end
      # I18n ja.ymlのfree_formの数に応じた項目を定義(ex. free_text_0: '備考1')
      I18n.backend.store_translations :ja, activemodel: {
        attributes: { 'inquiry': { "free_text_#{index}": value[:remark] } }
      }
    end
  end
end
```

## Controller
一応、入力→確認→送信 の問い合わせフォームのつもりですが、newとconfirmのバリデーション部分までで他は仮で作っています。
`inquiry_params`で備考欄の数に応じてStrong Parameterに追加するようにします。

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

  def new
    @free_form = FreeForm.all
    @inquiry = Inquiry.new(@free_form)
  end

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

  def create
    @free_form = FreeForm.all
    @inquiry = Inquiry.new(@free_form, inquiry_params(@free_form.count))
    @inquiry.save!
  end

  private

  # Strong Parameters
  def inquiry_params(count)
    free_texts_params = count.times.map { |index| "free_text_#{index}".to_sym }
    params.require(:inquiry).permit([:name, :tel, :email] << free_texts_params)
  end
end
```

## View
Modelに紐付いたフォームのため、備考欄も含めて`form_for`で定義しています。(※rails5.1からは`#form_with`が使えるようです。)
confirmとcreateのViewは省略。

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

  <% if @inquiry.errors.any? %>
    <div>
      <strong>入力内容にエラーがあります</strong>
      <ul>
        <% @inquiry.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% 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>
    <% @free_form.present? %>
      <% @free_form.each_with_index do |value, index| %>
        <tr>
          <th><%= f.label :"free_text_#{index}" %></th>
          <td><%= f.text_area :"free_text_#{index}", size: "50x5" %></td>
        </tr>
      <% end %>
  </table>
  <%= f.submit '確認' %>
<% end %>
```


## i18n
i18nで多言語化にも対応可能です。2行目に記載しているように`activemodel`と定義します。

```yaml:ja.yml
ja:
  activemodel:
    models:
      inquiry:
    attributes:
      inquiry:
        name: 名前
        tel: 電話番号
        email: メールアドレス

    errors:
      format: '%{attribute} %{message}'
      messages:
        blank: を入力して下さい。
        too_long: は%{count}文字以内で入力して下さい。
        not_a_number: は半角数字で入力して下さい。
        invalid: は不正な値です。
```