Rails

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ベースの簡単な問い合わせフォームです。このフォームの備考欄をテーブルのデータ数にあわせて、動的に表示します。

初期表示

2017-11-20_00h31_36.png

バリデーションエラー

Railsではデフォルトでバリデーションエラー時にラベルやフォームの要素をfield_with_errorsというclassのdivで囲んでくれるので、cssなどでそのclassを指定しておけばエラーが発生した項目に色を付けられます。
2017-11-20_00h40_24.png

可変部分のデータ

この場合、3件の備考欄を表示できるようにします。

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を使用してアクセサとバリデーションを定義しているところです。バリデーションエラーで色を付けるためにはここを通さないとダメっぽいので、アクセサの定義は必要です。
I18n.backend.store_translationsではテーブルの値に基づき新しい翻訳文を追加しています。

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に追加するようにします。

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は省略。

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と定義します。

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

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