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

バリデーションエラー
Railsではデフォルトでバリデーションエラー時にラベルやフォームの要素をfield_with_errors
というclassのdivで囲んでくれるので、cssなどでそのclassを指定しておけばエラーが発生した項目に色を付けられます。
可変部分のデータ
この場合、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
ではテーブルの値に基づき新しい翻訳文を追加しています。
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に追加するようにします。
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は省略。
<%= 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:
activemodel:
models:
inquiry:
attributes:
inquiry:
name: 名前
tel: 電話番号
email: メールアドレス
errors:
format: '%{attribute} %{message}'
messages:
blank: を入力して下さい。
too_long: は%{count}文字以内で入力して下さい。
not_a_number: は半角数字で入力して下さい。
invalid: は不正な値です。