Typically in Rails, forms which post data to a
#update action are concerned with one particular resource. For example, a form to update a user’s profile. The sequence looks something like this:
- The form submits some data to the
#createaction, with something like
User.create(params)creates the record in the
This is easy to reason about. We have one form to create one user. It submits user data to a users controller, which then creates a record in the User table with a call to the
Though some percentage of the forms in our app can be described this simply, most forms we find ourselves needing to build are not that simple. They might need to save multiple records, or update more than one table, and/or perform some additional processing (like sending an email). There might even be more than one context under which a record can be updated (for example a multi-step application form).
Trying to stuff a bunch of logic in your controller can turn out to be quite painful in the long run, as they become longer and harder to understand. Moreover, you might find yourself having to perform gymnastics with your views to get them to pass in parameters correctly.
For example, imagine you’re building a form for a project management app. This form creates a user like in our first example, but in addition, has to do a few other things. First, it has to create a record in the
Message table. Then it has to create a record in the Task table. Finally it has to send an email to the project manager.
Some definitions and assumptions before we go forward:
Messagetable is used for internal messaging amongst various members of the project.
Tasktable is used to store and manage tasks assigned to members of the project.
- When a new user is created, we want to send an internal message and assign a new task to the project manager.
- We also want to send an email notification to the project manager when a new user is created.
Compared to the first example, it’s obvious that this form requires a flow that doesn’t quite as neatly fit in to the conventional form flow that we saw earlier. There are a few ways we can handle this:
- Add in the code to
UsersControllerto accomplish the extra stuff.
- If your models are associated with each other (via
belongs_to), build a nested form using either Rails’ built-in
fields_forhelper, or Ryan Bates’ nested_form gem. You’ll still have to send the email in the controller.
- Create a new controller and corresponding “form object” that encapsulates what you want to do.
What is a Form Object
A form object is a Plain Old Ruby Object (PORO). It takes over from the controller wherever it needs to talk to the database and other parts of your app like mailers, service objects and so on. A form object typically functions together with a dedicated controller and a view.
Why use a Form Object
Now, while there are good reasons to go with either option 1 or 2, I’m going to elaborate on option 3. There are a few benefits to using a form object in a situation like this:
Your app will be easy to change.
If your app is a decent sized web-app, you will likely have a multitude of paths and views through which data gets saved and/or retrieved from your Users table. It’s worth thinking about if
Usermodel should be where these paths meet, because if you’re not careful your controller can devolve into a mess of hard-to-change conditional code.
Your app will be easy to reason about
Conventionally in Rails, the simplest way to reason about a controller is to have it concerned with the seven RESTful actions and views; these actions would only interact with one model and ideally the interaction would be a one-liner like
@user.update(params). The closer your controller is to this pattern, the easier your controller will be to reason about.
Your view will likely be simpler to write (read: no deeply nested forms) and contain less logic.
What a Form Object looks like in practice
First things first, decide on the name of your new controller. This will give you quick feedback on if your abstractions will make things easier to reason about.
I always ask myself this, what happens when the form is submitted? In our example above, a user is created and the rest of the project team is notified. So a reasonable name might be ‘UserRegistrationsController`. You can double check by trying to apply the seven RESTful actions to this controller:
- Can I “create” a user_registration?
- Can I “update” a user_registration?
- Can I “destroy” a user_registration?
- … and so on
All seven actions might not always make sense, which is why I find it handy to define my routes like this, for example:
resource :user_registrations, only: [:create, :update, :new, :edit]
In my Rails apps, the convention I follow is to always name the form object class
FormObject, and namespace them with something context dependent. So in this case, my form object would be
So your form, which resides at
user_registrations/new.html.erb, posts some data to the
#create action of the
UserRegistrationsController, which calls
UserRegistration::FormObject with the params you pass in.
Form Object parameters
To be able to specify an input in your form, you need to expose the related attribute in your form object. In our example, one of the inputs we want is the user’s name. So in our form object, we’d do:
module UserRegistration class FormObject include ActiveModel::Model attr_accessor :name ... ... def self.model_name ActiveModel::Name.new(self, nil, 'UserRegistration') end end end
Because we’ve said
attr_accessor :name, we can now in our view say something like
<%= f.text_field :name %>.
You’ll also notice a couple of other things:
include ActiveModel::Model. This is cool, because we can now use methods like
validatesand perform any validations we want. An advantage of using validations in the form object is that they are specific to the form object and won’t clutter up your model(s).
We also defined the
model_nameclass method. The Rails form builder methods (
form_forand the rest) need this to be defined.
Form Object Initialize and Save
The behavior of our form object will be governed by two methods.
#save. This is because in our controller, we want to be able define our create action like so:
def create @form_object = UserRegistration::FormObject.new(params) if @form_object.save ... #typically we flash and redirect here else render :new end end
You can see above how the form object directly replaces the model, allowing us to keep our controller clean and short.
Your initialize method would look something like:
def initialize(params) self.name = params.fetch(:name, '') ... # and so on and so forth end
... def save return false unless valid? #valid? comes from ActiveModel::Model User.create(name: name) notify_project_manager assign_task_to_project_manager true end private def notify_project_manager ... # here we talk to the Message model end def assign_task_to_project_manager ... # here we talk to the Task model end ... # and so on and so forth
The important thing with the
#save method is to return false and true correctly depending on if the form submission meets your criteria, so that your controller can make the right decision.
I’m a big fan of form objects, and a general rule of thumb for me is to use them whenever I feel things are getting complicated in the controller and view. They can even be used in situations where you’re not dealing directly with the database (like for example interacting with a third party API).
I encourage you, if you haven’t already, to consider how form objects might fit into your app. They will go a long way in ensuring your controllers and models are thin and keeping your code maintainable.
Have you used form objects in your Rails apps? Have they helped or hindered you? How else do you keep your controllers thin? Let me know in the comments section, I’d love to hear what you think.