LoginSignup
6

More than 5 years have passed since last update.

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

Posted at

はじめに

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

以前書いた、ActiveRecordを使わない動的なフォームとバリデーション の改良バージョンになります。

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

環境

Ruby : 2.4.2
Ruby on Rails : 5.1.4

作ったもの

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

初期表示

2018-01-17_00h22_24.png

バリデーションエラー時

子モデルのバリデーションエラーを特定し、エラーがあった入力エリアを赤くします。
2018-01-17_00h23_39.png

子モデルのデータ件数

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

sqlite> select count(*) from free_forms;
3

列数を動的に出力したかっただけで、このテーブルに意味はないです。

Model

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

[親のモデル]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する。


[子のモデル]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のバリデーション部分までで他は仮で作っています。

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 のブロックでは子モデルのインスタンス分、繰り返されます。

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| %>
    ...

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6