Rails Design Patterns: Form Object
Tags: ruby, rails, webdev
Published: 2021-06-02 17:04:47

# Form Object Pattern

There are a number of design patterns that will help improve your Rails app.  One such pattern is the Form Object. At its core a Form Object is just a Plain Old Ruby Object (PORO), but we can design the object in such a way that it will help us to maintain a consistent API in our code. Let's dive in and see how we can leverage a Form Object to improve our Rails App.

# The messaging feature

Let's say our boss asks us to add a messaging feature to our application. They tell us the message shouldn't be a database table, so no model. It just needs to be a form where a user can select another user, add a subject and body, and then send out an email. Sounds easy enough, let's get that added. (for simplicity let's say our app already has a `User` model with `first_name`, `last_name`, and `email` attributes, and a `DummyMailer` with a `message` method that takes `to`, `subject`, and `body` as keyword arguments)

First let's add the messages controller.
```ruby
# app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def show; end

  def create
    DummyMailer.message(message_params).deliver_later
    redirect_to messages_path, notice: "Your message was sent!"
  end

  private

  def message_params
    params.permit(:to, :subject, :body)
  end
end
```

Then we can add our view/form
```erb
# app/views/messages/show.html.erb
<h1>New Message</h1>

<h3><%= notice %></h3>

<%= form_with url: messages_path do |form| %>
  <div class="field">
    <%= form.label :to %>
    <%= form.select :to, User.all.pluck(:email), include_blank: "Select a recipient" %>
  </div>

  <div class="field">
    <%= form.label :subject %>
    <%= form.text_field :subject %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>

  <div class="actions">
    <%= form.submit "Create Message" %>
  </div>
<% end %>
```

After we add `resource :messages, only: %i[show create]` to our routes file we can start up our server and checkout our new form (forgive the lack of styling).

![Our New Form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fldsyg3h4glgou9u475b.png)

If we select a user, add a subject, and a body

![user, subject, and body](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ilc62ucxw901vioj4xqx.png)
 

We'll see things work out pretty well.

![It works!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uamldjgy0kr0vz6wgswk.png)
 

## Close but no cigar.

That is until we show it to our boss. First thing she does is to try submitting the form with no details and frowns when she sees "Your message was sent!" on the screen. She asks us to show an error message if any of the information is missing. Ok, slightly more complicated but still doable. Let's update our controller.

Something like:
```ruby
class MessagesController < ApplicationController
  def show; end

  def create
    if message_params[:to].present? && message_params[:subject].present? && message_params[:body].present?
      DummyMailer.message(message_params).deliver_later
      redirect_to messages_path, notice: "Your message was sent!"
    else
      flash.now[:notice] = "Please include all fields"
      render :show
    end
  end

  private

  def message_params
    params.permit(:to, :subject, :body)
  end
end
```

![Well, that's something](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0demc600psowbq3nvbse.png)
 

## More problems...

This works but our boss spots another problem almost immediately. If we include _some_ of the fields those fields are cleared out when we re-render the form. Our boss also wants us to limit the length of the `subject`, highlight the fields with errors, and list the issues when submitting an invalid form. Yikes! This is a lot more logic than we should have in a controller, but without a model where should we put it? Well, in a model of sorts. See, a model doesn't have to inherit from ActiveRecord _**or**_ be backed by the database. We can add a PORO, include `ActiveModel::Model`, and be off to the races.

## Form Object to the rescue!

```ruby
# app/models/message.rb
class Message
  include ActiveModel::Model

  attr_accessor :to, :subject, :body

  validates :to, :subject, :body, presence: true
  validates :subject, length: { maximum: 255 }
end
```
You'll notice I put this in the `models` directory. This is personal preference. It could have just as easily have been placed in a `forms` directory in `app` or even `models`.  There's absolutely nothing wrong with having "models" (which this arguably is) in your `models` directory.  They don't all have to be database-backed.

Armed with our new form object, let's update our form.
```erb
<h1>New Message</h1>

<h3><%= notice %></h3>

<%= form_with model: @message, url: messages_path do |form| %>
  <div class="field">
    <%= form.label :to %>
    <%= form.select :to, User.all.pluck(:email), include_blank: "Select a recipient" %>
  </div>

  <div class="field">
    <%= form.label :subject %>
    <%= form.text_field :subject %>
  </div>

  <div class="field">
    <%= form.label :body %>
    <%= form.text_area :body %>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>
```
Notice that we added `model: @message` to our `form_with` call. This is what will help give us all the form behavior that we've come to expect with Rails.  It also allows us to remove the specific "Create Message" label from the submit action since passing in a model will populate this for us. To make this _actually_ work though we'll need to make some updates to our controller.

```ruby
class MessagesController < ApplicationController
  def show
    @message = Message.new
  end

  def create
    @message = Message.new(message_params)

    if @message.valid?
      DummyMailer.message(message_params).deliver_later
      redirect_to messages_path, notice: "Your message was sent!"
    else
      flash.now[:notice] = @message.errors.full_messages.join(", ")
      render :show
    end
  end

  private

  def message_params
    params.require(:message).permit(:to, :subject, :body)
  end
end
```

Nice. This is looking _a lot_ more like a controller we'd expect to see in a Rails app. We're leveraging [Strong Params](https://api.rubyonrails.org/classes/ActionController/StrongParameters.html) correctly and letting our "model" take care of all the validation logic. With no other changes our form is now working as our boss requested:

![Looking good!](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r1d7geirbqeftdxu6tmh.png)
 

## Bonus round: Making a _really_ expected controller.

Theres one _small_ part of our controller that may stick out. The call of `valid?` vs `save` is different than what we'd expect to see in a "normal" Rails controller. I'm also not a fan of having the mailer call in the controller (but that may just be me).

```ruby
class MessagesController < ApplicationController
  def show
    @message = Message.new
  end

  def create
    @message = Message.new(message_params)

    if @message.save
      redirect_to messages_path, notice: "Your message was sent!"
    else
      flash.now[:notice] = @message.errors.full_messages.join(", ")
      render :show
    end
  end

  private

  def message_params
    params.require(:message).permit(:to, :subject, :body)
  end
end
```
If we try this now you'll get a "NoMethodError" error because, though `ActiveModel::Model` give us a lot of model logic, it does not give us a `save` message. No worries though, the simple act of adding a `save` method to our "model" will fix that, with the added bonus of giving us a good place for our mailer call (if you're into that kinda stuff).

```ruby
class Message
  include ActiveModel::Model

  attr_accessor :to, :subject, :body

  validates :to, :subject, :body, presence: true
  validates :subject, length: { maximum: 255 }

  def save
    return false unless valid?

    DummyMailer.message(to: to, subject: subject, body: body).deliver_later
  end
end
```

If we try it again now every thing will work. 🎉

Though this was a fairly simple example I hope it illustrated the awesomeness of a Form Object. Allowing our controllers to look, function, and behave in an expected way is a huge win and paramount to keeping our application stable and easily upgrade/expandable. We also now have a much more familiar form design. Validation is performed using battle-tested and well-known methods, not by recreating that logic. Any additional changes to our "Message" feature is now incredibly easy and will feel no different than adding features to a model backed by the database.