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 new and create actions:

# 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 form_with helper:

# 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!