想定読者
Railsを使ってポートフォリオを作成している方
特にクイズアプリを作成している方
どういうときの話
私はクイズアプリを作ってるときにこの問題に当たりました。
一つのフォーム画面で、
①問題文
②選択肢1
③選択肢2
④選択肢3
⑤選択肢4
を同時に登録させたいとします。
で、モデルはこんな感じです。
class Question < ApplicationRecord
has_many :choices, dependent: :destroy
end
class Choice < ApplicationRecord
belongs_to :question
end
class CreateChoices < ActiveRecord::Migration[5.2]
def change
create_table :choices do |t|
t.references :question, foreign_key: true
t.string :content, null: false
t.boolean :is_answer
t.timestamps
end
end
end
- 「問題 has many 選択肢s」です。
- 選択肢テーブルはis_answerという「正答か誤答か」というカラムを持っています。
よって単純なform_withとモデルでは実装できません。フォームに合わせてDB設計を変える、みたいなのは本末転倒です。(私の記事で恐縮ですが、クイズアプリのDB設計に関して、以下記事を書きました)
https://qiita.com/kumackey/items/7ccbc949458bd0af22bd
他にも、以下の例が思いつきます。
- 就活サイトのユーザ登録画面で、has manyな経歴を同時に登録する
- SNSのポスト投稿画面で、投稿自体とタグ複数を同時登録する
- レシピサイトで、レシピ名と、複数の手順を同時に登録する
accepts_nested_attributes_forではダメ?
非推奨らしいです。
実は私はaccepts_nested_attributes_forに関しては逆に詳しくないため、詳細は以下URL等をご覧いただければと思います。
https://moneyforward.com/engineers_blog/2018/12/15/formobject/
https://tech.recruit-mp.co.jp/server-side/rails-development-policy/
https://tech.libinc.co.jp/entry/2019/04/05/113000
フォームオブジェクトを作る
class RegisterQuizForm
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations
attribute :question_content, :string
attribute :correct_choice, :string
attribute :incorrect_choice_1, :string
attribute :incorrect_choice_2, :string
attribute :incorrect_choice_3, :string
validates :question_content, presence: true, length: { maximum: 140 }
validates :correct_choice, presence: true, length: { maximum: 40 }
validates :incorrect_choice_1, presence: true, length: { maximum: 40 }
validates :incorrect_choice_2, length: { maximum: 40 }
validates :incorrect_choice_3, length: { maximum: 40 }
def save
return false unless valid?
question = Question.new(content: question_content)
question.save # 問題文の登録
choice = question.choices.build(content: correct_choice, is_answer: true)
choice.save # 正解選択肢の保存
question.choices.create(content: incorrect_choice_1, is_answer: false)
question.choices.create(content: incorrect_choice_2, is_answer: false)
question.choices.create(content: incorrect_choice_3, is_answer: false)
# 不正解選択肢の登録
end
end
フォームオブジェクト自体に知見が無い方は、以下記事をご参照いただければと思います。
https://tech.medpeer.co.jp/entry/2017/05/09/070758
https://qiita.com/kamohicokamo/items/d2ea4d71f86d99261b1a
return false unless valid?
によって、ヴァリデーションが通らないときにfalseを返します。これにより、コントローラーで使用する際も、以下のような実装が可能になります。
if @register_quiz_form.save
(ヴァリデーションが通ってsaveに成功したときの処理)
else
(ヴァリデーションが通らずsaveに失敗したときの処理)
end
if @user.save
に近い感覚で使うことができます。
コントローラーとビュー
class QuestionsController < ApplicationController
def new
@register_quiz_form = RegisterQuizForm.new
end
def create
@register_quiz_form = RegisterQuizForm.new(create_question_params)
if @register_quiz_form.save
redirect_to (成功したときのパス)
else
render :new
end
end
def create_question_params
params.require(:register_quiz_form).permit(
:question_content, :correct_choice,
:incorrect_choice_1, :incorrect_choice_2, :incorrect_choice_3)
end
end
※ 以下Viewですが、筆者がslimに慣れているのでslimで記述しています。erbに置き換えてお読み頂けると幸いです。
= form_with model: @register_quiz_form, url: questions_path, local: true do |f|
= render '(エラーメッセージ表示用のパーシャル)', object: @register_quiz_form
.form-group
= f.label :question_content
= f.text_field :question_content, class: 'form-control'
.form-group
= f.label :correct_choice
= f.text_field :correct_choice, class: 'form-control'
.form-group
= f.label :incorrect_choice_1
= f.text_field :incorrect_choice_1, class: 'form-control'
.form-group
= f.label :incorrect_choice_2
= f.text_field :incorrect_choice_2, class: 'form-control'
.form-group
= f.label :incorrect_choice_3
= f.text_field :incorrect_choice_3, class: 'form-control'
= f.submit '登録する', class: 'btn btn-raised btn-success'
ちなみにフォームブジェクトという設計は、ActiveRecordのattributesと、フォーム画面で表示させたいことが異なる、というケースのいずれでも効果を発揮します。
私はフォームオブジェクトが大好きすぎて、ポートフォリオには3つのフォームオブジェクトがあります。笑
間違い等あればご指摘頂けると嬉しいです。