Disciplined Rails: Form Object Techniques & Patterns — Part 1

Ground rules for form objects

Jaryl Sim
11 min readJul 9, 2018

Part 1: Ground rules for form objects
Part 2: Dealing with collections
Part 3: Tying up loose ends

I can’t remember when I first learned about form objects, but I imagine coming across as many implementations of form objects as there were articles written for it. What I hope to achieve with this multi-part series is a bit different; it should serve as an encyclopaedia of techniques and patterns used to build form objects on rails. Okay, so encyclopaedia is a bit of a tall order, but my hope is to have a comprehensive catalog of all things form object related. If I’ve missed out something, I’m sure you’ll let me know and I’ll add it in.

For part 1, I’ll start off with some foundations, but you can gloss over that if you are already using form objects in your everyday work. Once we’re through with that, we can go to the heavy lifting, where we deal with multiple models, nested models, complex search forms, etc in the other parts of the series.

I’ve created a repository for this article, which you can use to follow the code more closely here:

Why Form Objects?

Before proceeding, we should set the record straight on a couple of things. One common use of form objects is to reduce complexity in the controller, and the model, which isn’t incorrect, but does not do justice to the important role that they play.

Moving responsibilities to the form object reduces decoupling between the controller, model (including multiple models), and the view template. When deciding whether or not use a form object, you can evaluate the following statements:

I have multiple models, and validation/persistence logic that shouldn’t reside in any single one of the models.

As much as possible, you don’t want your models to know about each other beyond the associations, which we can consider to be persistence logic. Business logic that spans across multiple models are best kept outside of the ActiveRecord models (form objects are models too, so let’s make the distinction clear). More on this later, if you aren’t convinced.

I have a bunch of query logic that is clogging up the controller, and moving it to the (ActiveRecord) model feels wrong (it usually is).

You have a search form, and it takes half a dozen parameters, and your controller is starting to look like it was touched by the Flying Spaghetti Monster itself. The more parameters you have, the more branches your controller spirals (high cyclomatic complexity) into and you’ve probably given up writing specs at this point.

I’m sure that there are other cases where you would want to use form objects, but these are a couple of smoking gun situations where you need to drop in a form object on the double.

A form object before it is about to be touched by His Noodly Appendage.

How is Babby Formed

Don’t learn how to create form objects from Yahoo! Answers.

Skip this section if you are already a form object rock star (or if you really are one, you could just skip the whole series duh). Otherwise, we start with some boilerplate code for an email enquiry form, which we will dive right into.

class ContactForm  include ActiveModel::Model  attr_accessor :name, :email, :message
validates :name, :email, :message, presence: true
def submit
return false if invalid?
# send acknowledgement reply, and admin notification emails, etc
true
end
end

Notice that we suffix the class name with ‘Form’, I also move all form classes into the app/forms folder in a rails app. In a smaller app, you can get away with putting it in the app/models directory, especially with the techniques on emulating an ActiveRecord model, but at some point you are going have to deal with multiple types of models in your app, not just ActiveRecord and Form objects, but that’s a topic for another time.

When we include ActiveModel::Model we get a bunch of free functionality. For example, now you can do this:

ContactForm.new(name: 'John Doe', email: 'john.doe@example.net')

Note: if you override your initialize method, make sure to call super like this to preserve this functionality:

def initialize(params = {})
super(params)
end

Just make sure that you have the necessary accessor methods to actually perform the assignment. In addition, ActiveModel gives you the same validators that you’ve grown accustomed to in rails, which I won’t explain it here, but we will take a look at more complex validation needs later on in the series.

Last thing of interest is the submit method, which is where we do the actual work, which we should note be the only method that has side effects. In this case, it’s a simple enquiry form, and it is up to you if you want to save a record to the database, or if you just want to send emails.

There’s bound to be some debate on whether we call the method that does the work submit or save, which is what we are used to in rails land, and I’ve practiced personally on older projects. It boils down to your team’s preference really, but there are many times where we don’t actually save anything to the database, like in a form object for a search page, or like this very use case of sending an email (unless you consider sending the email as saving it on the server…), so we made the decision to call it submit instead and it stuck.

With this form object, we have decoupled the view template with the actual work being done. If the work was being done in the controller, then validation becomes a huge pain, and we wouldn’t have the utilities that ActiveModel affords us to pass validation errors easily to the view template. It’s worth repeating that it works how you expect any ActiveModel/ActiveRecord model works.

With that, your controller looks now like this:

def new
@form = ContactForm.new
end
def create
@form = ContactForm.new(contact_form_params)
if @form.submit
redirect_to root_path, notice: 'Thank you for your enquiry'
else
render :new
end
end
privatedef contact_form_params
params.require(:contact_form).permit(:name, :email, :message)
end

Which is what you want all your controller actions to look like. I wrote more about this in my other article on models and controllers in my Disciplined Rails series. As a matter of convention, I also use the instance variable name @form to refer to the form object instance.

It’s fine if you want to name it @enquiry or something else, but I’ve never been happy with off-the-cuff naming conventions I‘ve seen with when it comes to naming form objects. You may argue that we lose information in naming it as such, but you can easily access that from its context of being used in a new/edit template, or in a controller action.

If you think that calling it @form is asinine, then you would also balk at having :contact_form as the name of the form, and param key that is submitted. To you the only thing for it to be called that makes sense is :enquiry, nothing else makes sense. Okay okay, I don’t agree with you, but I got your back. To accomplish this you can add a class method to your form object like so:

def self.model_name
ActiveModel::Name.new(self, nil, 'Enquiry')
end

This is mostly a matter of preference, one benefit would be that you can rely on the rails default behavior of inferring the form action target from the model itself, and you do not have to pass in the URL property to your form. However, I don’t mind being a bit more explicit about where a form is going to submit to.

To me, it makes sense because the form object models the form in HTML, and adhering to the ActiveRecord interface is well-intentioned, but more of a red herring than anything. Naming it based on your ActiveRecord model falls apart when your form object starts to deal with not just that model, but another one, or any other concern that has nothing to do with the original model.

The book every developer wishes their team read.

While it is useful to name elements of your interface in a semantically pleasing manner, naming thing arbitrarily tends toward eventual chaos unless you conscientiously work define your naming conventions. It’s fine if you live with the natural form name, and it definitely makes it easier to look up the actual form object just by following the code. After all, one of the two hardest things in Computer Science is naming things; save your artful deliberation for APIs.

Dealing with an ActiveRecord Model

With our boilerplate example, we didn’t actually need to work with the database on anything, and you’re now shaking your head thinking ‘creating enquiry forms don’t pay the bills yo’, and really I hear you man.

So let’s start with an example I’ve taken directly from the guides.

class Person < ApplicationRecord
validates :terms_of_service, acceptance: { message: 'must be abided' }
end

If you look at your own User model, where you already have a hundred lines worth of business logic that you absolutely need for it to be there (which is a debate for another time), and now the legal department needs you to add this one little checkbox (and a bunch of other things that are out of scope of this article) to the registration form before the 25th May 2018, so you copy/paste the above badass into your User model.

You then add on: :create because you are smart like that, and all is good in the world, and this is probably going to be fine for a while. But what if you had a lot of other things that you want to validate, and you only need it on create, or at some special state that the user account is in. Over time, cruft builds up, some belonging to new user registrations, and others to updating accounts, changing passwords, being activated/deactivated, etc.

When you try to test some behavior in the rails console, you are inundated with these extraneous concerns that you need to wade through before you can actually test what you want. Even if you don’t give up, you don’t even bother using the rails console for subsequent attempts.

If that sounds like you (I speak from experience) then the good news is that yes you can also move some of this registration logic to your form object. Now it looks like this:

class UserRegistrationForm  include ActiveModel::Model  attr_accessor :person, :terms_of_service  delegate :attributes=, to: :person, prefix: true  validates :terms_of_service, acceptance: true
validate :person_is_valid
def initialize(params= {})
@person = Person.new
super(params)
@terms_of_service ||= false
end
def submit
return false if invalid?
person.save
end
private def person_is_valid
errors.add(:person, 'is invalid') if person.invalid?
end
end

So I cheated a little, our form object here doesn’t persist the terms of service checkbox value in the database like it would in the rails guides example. However, I don’t think it was necessary since we never actually allow users to register if they hadn’t checked that option on the form.

This concession allows to move some logic outside of the model, which makes sense since it is a piece that doesn’t really qualify as a persistence concern. Form objects are great for allowing us to move non-persistence logic outside of the ActiveRecord model.

What may have caught your interest is the use of the delegate method, which here allows us to access the person object, and assign values to its attributes accordingly. Doing so effectively satisfies the mechanics of nested attributes, which works when rails detects the person_attributes= method’s presence.

This technique of mimicking nested_attributes may seem overly magical quite frankly, but the alternative idea of rolling my own code to handle attributes excites me less.

The other thing that would have caught your attention is the need to trigger the validations on the person model, which I’ve done using a custom validation on the form object itself. This is important, as you want to strap in with the form’s validation lifecycle, rather than running validations yourself somewhere else.

Do note that while we sort of bubble up the error message to the form‘s error hash, which we can then display in a flash message at the top of the form, this also triggers errors to be created for each field, on the person object, which is useful for displaying errors on your form inputs later on.

To round this up, you will also need your view template and controller to get in line, controller shown here first:

def new
@form = UserRegistrationForm.new
end
def create
@form = UserRegistrationForm.new(user_registration_form_params)
if @form.submit
redirect_to root_path, notice: 'Thank you for your registration'
else
flash[:error] = @form.errors.full_messages.to_sentence
render :new
end
end
privatedef user_registration_form_params
params.require(:user_registration_form).permit(:terms_of_service, person_attributes: [:name, :email, :password, :password_confirmation])
end

Your view would look something like this (I assume the use of slim and simple_form because that’s what I am used to):

= simple_form_for @form, url: users_path do |f|  = f.input :terms_of_service, as: :boolean  = f.simple_fields_for :person do |person_fields|    = person_fields.input :name
= person_fields.input :email
= person_fields.input :password
= person_fields.input :password_confirmation
= f.submit

Since we use attr_accessor to define person, then the form helper can retrieve the person object automatically to fill up the above fields, including error states and messages, even when nested. Do note that if we hadn’t set up a default value in the form object’s initializer, the fields for person would not even show up.

As an addendum, if for some reason you need to work with two independent models at a time, you can just add the new model at the same level, and connect them up in the submit method, like so:

class UserRegistrationForm  # ...  attr_accessor :person, :group, :terms_of_service  delegate :attributes=, to: :person, prefix: true
delegate :attributes=, to: :group, prefix: true
# ... def submit
return false if invalid?
group.people << person
person.save
end
# ...end

This technique is useful for flattening some pretty complex model hierarchies. Instead of having deeply nested form fields, which are then tightly coupled with your model hierarchy, you can define the interface you want between form object and view template.

In general, I find it useful to write the view template that I want first, and then line up the things that it needs in the form object (or presenter, decorator, etc).

To sum things up for part 1, I’ve sought to cover the scenarios for when we would introduce a form object into a code base. The major motivations for doing so include dealing with multiple models, and avoiding coupling across the models and controllers.

Of course, we also use form objects when there are no ActiveRecord models to work with, such as in the first example, where we are merely sending emails. No real choice in this case, as the alternative is to dump logic into the controller.

We’ve also covered one way to work with ActiveRecord objects and ActiveModel validations, using nested entity under the form object, including passing parameters and validation results.

Head over to part 2, where will deal with collections of objects.

--

--

Responses (4)