概要
原著者の許諾を得て翻訳・公開いたします。
タイトルは内容に即したものに変えました。
Rails: Form Objectと#to_model
を使ってバリデーションをモデルから分離する(翻訳)
Rails 5.2リリースノートをひととおり読んでみて、ActiveStorageなどに興味を惹かれたので、試してみたくなりました。本記事ではプレリリース版を用いてシンプルなアプリをビルドしました。
目的は、ユーザーがアンケート(questionnaire)を作成して結果を回収できるアプリを作成することです。最初にForm Objectを用いて、アンケートのタイトルと質問リストを取得します。
# db/migrate/create_questionnaires.rb
create_table "questionnaires", force: :cascade do |t|
t.string "title"
t.string "questions", default: [], null: false, array: true
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
# app/forms/new_questionnaire_form.rb
class NewQuestionnaireForm
include ActiveModel::Model
attr_accessor :title, :questions
validates :title, presence: true
def save
Questionnaire.new(title: title, questions: questions).save
end
end
このように書いてみたかった理由は、Ectoを見たときに、バリデーションをモデルから切り離せるのがとても便利だと思えたからです(条件付きバリデーションが不要になります!)。
コントローラはいたってシンプルで、scaffoldしたコントローラと大差ありません。以下はnew
アクションとcreate
アクションです。
# app/controllers/questionnaires_controller.rb
class QuestionnairesController < ApplicationController
def new
@questionnaire = NewQuestionnaireForm.new
end
def create
@questionnaire = NewQuestionnaireForm.new(questionnaire_params)
if @questionnaire.save
redirect_to @questionnaire, notice: 'Questionnaire was successfully created.'
else
render :new
end
end
end
ビューでは、新しいform_with
ヘルパーを次のように使っています。
<!-- app/views/questionnaires/_form.html.erb -->
= form_with(model: @questionnaire, local: true) do |form|
= form.label :title
= form.text_field :title
アプリを起動してquestionnaires/new
にアクセスしてみると、undefined method 'new_questionnaire_forms_path' for ...
エラーメッセージが表示されました...。
うう残念。Railsは、form_with
に渡したForm Objectのクラス名を受け取ると、対応するコントローラへのパスであると自動的に推論しますが、コントローラの名前だけが合っていません。
この修正方法はいくつか考えられます。form_with
ヘルパーの投稿先のURLを上書きする方法もあれば、Form Objectの#model_name
を上書きして、今扱っているのがQuestionnaire
であるかのように見せかける方法もあります(この方法が有用なこともありますが、この状況ではダーティハックの恐れがあります)。
もっとよい方法を見つけるために、先ほどのForm Objectに立ち戻りましょう。このForm Objectは、Questionnaire
という単一のオブジェクトだけを扱っています。しかも#save
メソッドでQuestionnaire
を作成しています。先のForm Objectからモデルへの変換をシャドウイング(shadowing)として表すことができそうです。そしてActiveModel::Conversion#to_model
というメソッドがあることに気が付きました。このメソッドを使うようForm Objectを書き直すと次のようになります。
class NewQuestionnaireForm
include ActiveModel::Model
def to_model
Questionnaire.new(title: title, questions: questions)
end
def save
to_model.save
end
end
questionnaires/new
にアクセスすると、今度はちゃんとフォームが表示されます。タイトルを入力して[Submit]ボタンを押すと、データベースにquestionnaireのモデルが新しく作成されます。できました!