Routing form objects with Rails
Reading through the Release Notes of Rails 5.2, things like ActiveStorage made me curious so that I wanted to give it a try. I went ahead and installed a pre-release version to build a simple app.
My goal was to create an application that allows a user to create questionnaires and then collect answers. I started out with a form object, that would take the title of a questionnaire and a list of questions.
# 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
Why would I wanna do that? I saw in Ecto how convenient it is to separate validations from your models (no more conditional validations!).
Our controller is super simple and not much different to a scaffolded controller. Here are the
# 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
In our view we’re using the new
# app/views/questionnaires/_form.html.erb = form_with(model: @questionnaire, local: true) do |form| = form.label :title = form.text_field :title
If you start that up and visit the
questionnaires/new page, you’re greeted with an error message:
undefined method 'new_questionnaire_forms_path' for ….
What a bummer! Rails takes the class name of our form object we pass into
form_with and automatically infers a path to the corresponding controller - only that our controller has a different name.
Now, we have several possibilities to fix that: We could overwrite the URL we’re posting to in the
form_with helper. We could also override the
#model_name of our form object and make it appear like we’re dealing with a
Questionnaire (while this is valuable sometimes, I’d consider it a dirty hack for our situation).
To find a better solution, let’s have a look back at our form object. It only deals with a single object, a
Questionnaire. It even creates one in the
#save method. We might be able to express a conversion from our form object to the model it’s shadowing. Turns out, there’s
ActiveRecord::Conversion#to_model. Let’s rewrite our form object to make use of that method:
class NewQuestionnaireForm include ActiveModel::Model def to_model Questionnaire.new(title: title, questions: questions) end def save to_model.save end end
If we now visit
questionnaires/new, we’re greeted with a form. Filling in the title and hitting “Submit” will in fact create a new questionnaire model in our database. Horray!