はじめに
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対多の関連を持つフォームです。
初期表示
バリデーションエラー時
子モデルのバリデーションエラーを特定し、エラーがあった入力エリアを赤くします。
子モデルのデータ件数
この場合、3列分の子モデルを画面に表示できるようにします。
sqlite> select count(*) from free_forms;
3
列数を動的に出力したかっただけで、このテーブルに意味はないです。
Model
ActiveModel::Model
をincludeすることでActiveRecordと同じような感じでバリデーションが定義できます。
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する。
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のバリデーション部分までで他は仮で作っています。
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
のブロックでは子モデルのインスタンス分、繰り返されます。
<%= 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| %>
...