<-

Form Objects and Active Admin: There is a way!

Tam Eastley and Irmela Göhl · 03 Jul 2019

We introduced form objects into our code base about a year ago. We have a handful of them, but we realized that despite being similar, they’re all implemented in slightly different ways. We’d like to convert more of our code into form objects, but we first thought it would be a good idea to take a look at the ones we have and try to streamline them a bit. Refactoring seemed easy enough, but there’s one issue to take into consideration - we use Active Admin.

via GIPHY

Active Admin is great for quickly putting together simple CRUD based user interfaces, but as soon as your forms become more complex, things start to get tricky. Soon you’ll find yourself with lots of form-specific code in places it doesn’t belong and things can quickly get out of hand.

Introduction

What are we going to talk about?

We are using using Rails 5.2 and Active Admin 2.0. This blogpost will focus on how to implement form objects with Active Admin. There are a few different ways to use them, and they correspond to your controller actions: new/create, and edit/update. We’re just going to take a look at the new and create actions for now. In the future we hope to publish some more blogposts about edit/update, as well as form objects with nested resources. We are also assuming you already know what a form object is. If you don’t, this is a good (albeit a slightly out of date) introduction.

Why make the switch?

One of the reasons why we wanted to make the switch to form objects was because we had a few controllers like this one:

ActiveAdmin.register Issue do
  belongs_to :project
  actions :new, :create

  permit_params :title, :short_description, :description

  controller do
    def create
      if description.empty?
        redirect_back fallback_location: parent_url,
                      flash: { error: I18n.t("active_admin.issues.description_required") }
      end

      if short_description.empty?
        redirect_back fallback_location: parent_url,
                      flash: { error: I18n.t("active_admin.issues.short_description_required") }
      end

      create! do |success, failure|
        success.html { redirect_to parent_url }
        failure.html { render :new }
      end
    end
  end
end

We’ve had to add some “fake” validations in our create method in order to make sure the Issue has a description and short description. This is only needed when creating a new Issue via the UI. We don’t want to add model validations because maybe we don’t care if all of our old issues have descriptions, or maybe this isn’t needed when creating an issue via the Api. Controllers like this are annoying to test (especially as they grow) and are just begging to be turned into form objects, which are nice and compact and can be tested easily.

Another reason we wanted to make the switch is because we don’t like using accepts_nested_attributes_for. In the words of one of our colleagues, accepts_nested_attributes_for:

[…] adds methods to the model which only serve to help with the forms. But why should a model care about forms?

Moving to form objects enables us to get rid of this and we can can still persist multiple records in one form.

Our old colleague, Tobi Pfeiffer, gave a great talk at Ruby on Ice and touched on form objects and when they’re a good idea. His talk is well worth a watch (also because of the bunny pictures).

Let’s get started

What is Active Admin doing?

One of the big problems we initially ran up against when trying to implement form objects was weeding out what is Active Admin behaviour and what is actually something else.

One of Active Admin’s strong points is drying up your controllers. If you’re just using all the basic actions in your controller and you’re not doing anything beyond that, you don’t actually have to write any of those actions. Your controller will be essentially empty. However, this magic doesn’t come from Active Admin. It comes from Inherited Resources which is a dependency of Active Admin and which is also now maintained by the Active Admin organization. And if you want to use form objects with Active Admin, you might find yourself digging through the Inherited Resources source code every once in a while. Fortunately, Active Admin’s methods have inline documentation and (at least in our case) explicitly tell you when they are using Inherited Resources.

Using build_new_resource

In a standard Rails project that uses form objects, you are able to just define and instantiate your form object in the new and create actions of your controller. However, as we’ve just learned, Inherited Resources gives you all of these controller actions already, so you can either overwrite them and include your form object in them, or instead you can just overwrite Active Admin’s build_new_resource method. This is what we are doing and an example of how we refactored our controllers to incorporate form objects:


ActiveAdmin.register Issue do
  belongs_to :project
  actions :new, :create, :index

  permit_params :title, :short_description, :description

  controller do
    def create
      create! do |success, failure|
        success.html { redirect_to parent_url }
        failure.html { render :new }
      end
    end

    private

    def build_new_resource
      # This is our form object and it will handle our validations
      IssueForm.new(issue_params)
    end

    def issue_params
      # to be discussed
    end
  end
end

Doing this makes sure your new and create actions provided by Inherited Resources are using your form object. According to the docs, this method “uses the method_for_build provided by inherited resources”. The Inherited Resources docs say that method_for_build “returns the appropriated method to build the resource”. A resource here is the object you’re handling in your in your controller, aka, your form object.

Handling your params

So you’ve overwritten build_new_resource and you’re ready to submit your form, but what happens to all your params? We need to pass them to your form object.

Prior to our refactoring, we had handled our params differently across a few of our form object controllers. As you can see in the examples below, in some places we were using ActionController::Parameters#permit, in some places we were using Active Admin’s permit_params, in another place we were just accessing keys from the params hash, and somewhere else we were using Active Admin’s resource_params (this will be discussed below).

Accessing params directly from the params hash:

ActiveAdmin.register Issue do
  belongs_to :project
  actions :new, :create, :index

  # Notice we didn't use Active Admin's permit_params here

  controller do
    def create
      # this doesn't change
    end

    private

    def build_new_resource
      # this doesn't change
    end

    def issue_params
      issue_params = {
                        title:             params[:issue_form][:title],
                        short_description: params[:issue_form][:short_description],
                        description:       params[:issue_form][:description]
                      }
      additional_params = { project: parent }
      additional_params.merge(issue_params)
    end
  end
end

Using Active Admin’s resource_params:

ActiveAdmin.register Issue do
  belongs_to :project
  actions :new, :create, :index

  # Here we are using Active Admin's permit_params
  permit_params :title, :short_description, :description

  controller do
    def create
      # this doesn't change
    end

    private

    def build_new_resource
      # this doesn't change
    end

    def issue_params
      additional_params = { project: parent }
      issue_params   = resource_params.first

      additional_params.merge(issue_params)
    end
  end
end

Using ActionController:Parameters#permit:

ActiveAdmin.register Issue do
  belongs_to :project
  actions :new, :create, :index

  # We also didn't use Active Admin's permit_params here

  controller do
    def create
      # this doesn't change
    end

    private

    def build_new_resource
      # this doesn't change
    end

    def issue_params
      issue_params = if params[:issue_form].present?
                        params.require(:issue_form).permit(
                          :project, :title, :short_description, :description
                        )
                      else
                        {}
                      end
      additional_params = { project: parent }
      additional_params.merge(issue_params)
    end
  end
end

We were obviously doing this so many different ways, and every time we added a new form object we had to struggle with which never-really-defined guideline to follow. After some experimentation we decided to embrace, instead of fight, Active Admin, and came up with the following solution.

ActiveAdmin.register Issue do
  belongs_to :project
  actions :new, :create, :index

  permit_params :title, :short_description, :description

  controller do
    def create
      # this doesn't change
    end

    private

    def build_new_resource
      # this doesn't change
    end

    def issue_params
      additional_params = { project: parent }

      (permitted_params[:issue_form] || {}).merge(additional_params)
    end
  end
end

With this solution we could remove a lot of code, we were using Active Admin’s built-in behaviour, and we were handling both new and create actions. This solution was easily implemented across all our controllers.

via GIPHY

What we learned

In the process of figuring this out, we got super familiar with permit_params. When you add it to the top of your Active Admin controller, this defines for you a permitted_params method which returns your params to you as an ActionController::Parameters object. Interestingly, permitted_params comes from Inherited Resources. According to the docs:

If your controller defines a method named permitted_params, InheritedResources will call it where it would normally call params.

Sometimes we weren’t using permit_params, but using this helper was a big step in allowing us to refactor everything nicely and make our controllers super clear. This is yet another place where Active Admin and Inherited Resources are heavily intertwined.

We also had to keep in mind that build_new_resource is used both for the new and the create action, so we need to be able to handle cases where params are present, and where they are not.

We also spent some time looking at Inherited Resource’s resource_params method, which returns the permitted parameters in an array. For example:

[ActionController::Parameters{"name" => "Amy Santiago", "email" => "brooklyn_99@example.com}]`)

Without our gained knowledge of permitted_params, we were sometimes trying to access our params by calling resource_params.first which doesn’t look or feel very nice. We’re glad that we can get rid of that. The docs also suggest overriding this, but this seems unnecessary when using Active Admin’s permit_params method.

Conclusion

After making a plan and discussing refactoring our form objects, we were surprised to see that we hadn’t touched our form objects at all! We’d just touched our controllers and how Active Admin integrates form objects. This means that our form objects are seriously decoupled from Active Admin, which is great for testing, and also if we someday decide to move parts of our code away from Active Admin.