Today I Learned

Form objects with Active Admin

In this example, I’m using form object’s implementation from Selleo/pattern, but the solution shouldn’t differ much for other popular implementations.

If you want to use a form object class with the Active Admin, there are a few things you need to take care of:

  • Overwrite controller’s build_new_resource and find_resource methods.
    ActiveAdmin uses build_new_resource to build a new resource for new and create actions, and find_resource for retrieving resource in edit, update and show.
    We don’t want to use our form object in show, so we check action_name in find_new_resource.
contoller do
  def build_new_resource
    OrderForm.new(super)
  end
  
  def find_resource
    return OrderForm.new(super) if action_name.in?(%w[edit update])
    super
  end
end
  • Permit all params
    We don’t want to duplicate our form’s responsibility with Rails’ Strong Parameters.
controller do
  before_action -> { params.permit! }
end
  • Either overwrite the default form, or extend it with the names of your attributes, as it won’t be able to infer them automatically from the form object’s class.
form do |f|
  f.semantic_errors
  f.inputs(*OrderForm.attributes_names)
  f.actions
end

Repeating all of that for every page where you want to use a form object can get tedious, so here’s a dynamic concern which you can use to include those changes to your page quickly:

module UsesFormObject
  module_function

  def with_class(form_object_class)
    Module.new do
      define_singleton_method(:included) do |activeadmin_dsl|
        activeadmin_dsl.instance_eval do
          form do |f|
            f.semantic_errors
            f.inputs(*form_object_class.attributes_names)
            f.actions
          end

          controller do
            before_action -> { params.permit! }

            private

            define_method(:build_new_resource) do
              form_object_class.new(super())
            end

            define_method(:find_resource) do
              return form_object_class.new(super()) if action_name.in?(%w[edit update])

              super()
            end
          end
        end
      end
    end
  end
end

You can use it like that:

ActiveAdmin.register Order do
  include UsesFormObject.with_class(OrderForm)
end