How to Handle Singletons in ActiveAdmin

Ryan Stenberg, Former Developer

Article Category: #Code

Posted on

How to responsibly and gracefully implement singleton resource support in ActiveAdmin.

Singleton Struggles #

After spending a few hours digging through ActiveAdmin and Inherited Resources trying to figure out how to best support a singleton resource, I came to the following conclusion:

Inherited Resources has some singleton support baked in, but ActiveAdmin mostly disregards it. The way Inherited Resources wants to manage singletons gets mostly gobled up by ActiveAdmin assumptions about what a resource is and what's required for any given resource.

If you're curious, triggering Inherited Resource's singleton functionality is as simple as the following:

ActiveAdmin.register HomePage do
  controller do
    defaults singleton: true
  end
end

This preserves all actions aside from :index, sets up some other internal configuration, and includes some singleton-specific helpers (relevant code bits). The latter ended up getting me an undefined method :home_page for <Class.. coming from a singleton-specific resource finder method. It was trying to call HomePage.home_page, which wasn't a thing. To get the finder to function, I could either write the expected method or override resource.

Decisions. Throughout this process, there were a number of decisions to make -- mainly about what to compromise in order to get things working. Well, I knew what I definitely didn't want:

  • Unused controller actions and routes
  • Having to hack into obscure internals
  • Redirecting from unwanted controller actions to singleton-specific actions (ex: redirecting from index to show).
  • Using register_page. Singletons are resources, so I should have access to the resource DSL in ActiveAdmin.

Given my general goals and given all my digging, tinkering, and double-checking Google for others' thoughts and experiences, here are the steps for what I believe is the most responsible, graceful way to implement singleton resources in ActiveAdmin.

The Path to Singleton Sensibility #

For the following steps and any examples, let's assume the following:

  • Our singleton model is HomePage
  • We're using the default ActiveAdmin namespace :admin
  • We want admins to land on a show page
  • We only want to allow editing of our singleton, no creation or deletion.
  • The ActiveAdmin.register block is only duplicated in each snippet to show context. It's only actually needed once at the top of your ActiveAdmin resource file.

If you're attempting to reproduce this configuration/setup in your own application, make note that things may be different for you so you'll have to adjust accordingly.

Step 1: The Menu #

ActiveAdmin infers both the label and the target URL for a resource's menu item. With Singletons, both those things are different. The label shouldn't be pluralized and the URL shouldn't go to an index page. Fortunately, ActiveAdmin's menu DSL method already allows us to modify both of those things:

ActiveAdmin.register HomePage do
  menu parent: 'Content',
       label:  'Home Page',
       url:    -> { url_for [:admin, :home_page] }
end

Step 2: The Actions #

The actions DSL method in ActiveAdmin actually dumps directly through to an Inherited Resources class-level controller method that basically undefines methods from controllers. That's why if you omit the actions DSL method, you get the normal set of actions and their corresponding routes. To keep only the show, edit, and update actions, we'd write the following:

ActiveAdmin.register HomePage do
  actions :show, :edit, :update
end

Step 3: The Form #

Pretty much everything about the form works fine with singletons, outside of the cancel button that's automatically generated via the actions method in the context of a form block. By default, the cancel button links back to the index page for the resource. The actions method basically calls two things: action(:submit) and cancel_link (without arguments).

Since we're targeting the show page as our "landing page" so to speak, we need to modify the URL for the cancel button. Customizing the actions section of a form is part of the exposed DSL for ActiveAdmin forms, so it's actually really easy to update:

ActiveAdmin.register HomePage do
  form do |f|
    inputs do
      # all the inputs

      actions do
        action :submit
        cancel_link [:admin, :home_page]
      end
    end
  end
end

Step 4: The Controller #

We need our controller to be able to find our singleton resource and use URL helpers that point to our singleton routes. Re-visiting the Inherited Resources singleton functionality discussed earlier, we need to decide how to make resource function correctly. Since this is an admin controller concern, I think it's best to handle it in the admin controller rather than adding admin-related functionality to the model. We'll write our own resource method:

ActiveAdmin.register HomePage do
  controller do
    defaults singleton: true

    def resource
      @resource ||= HomePage.first
    end
  end
end

Without defaults singleton: true, our singleton URLs get the singleton resource's ID appended to them (ex: /admin/home_page/edit.1), causing ActionController::UnknownFormat errors.

Step 5: The Routes #

When ActiveAdmin loads, it examines all the resource configurations, checks each's actions, and builds up the appropriate routes for each resource. ActiveAdmin stubbornly uses the resources routes DSL method for this (code bits), so we'll always end up with the following routes given our actions declaration inside of our ActiveAdmin.register block (only show, edit, and update):

admin_home_page      : GET /admin/home_pages/:id        # :show
admin_home_page      : PATCH /admin/home_pages/:id      # :update
edit_admin_home_page : GET /admin/home_pages/:id/edit   # :edit

When we ideally want:

admin_home_page      : GET /admin/home_page
admin_home_page      : PATCH /admin/home_page
edit_admin_home_page : GET /admin/home_page/edit

This ensures that when our route helpers (admin_home_page and edit_admin_home_page) are called by ActiveAdmin internals, it finds our hand-written routes before the ActiveAdmin-generated routes. Having duplicate, unused routes for show, edit, and update isn't ideal. However, the alternative would have been writing the controller actions and action items from scratch, defeating much of the purpose of moving towards an ActiveAdmin resource rather than using an ActiveAdmin page.

The End Result #

To give a complete picture, I've put all the singleton-related code bits together.

Our app/admin/home_page.rb:

ActiveAdmin.register HomePage do
  menu parent: 'Content',
       label:  'Home Page',
       url:    -> { url_for [:admin, :home_page] }

  actions :show, :edit, :update

  form do |f|
    inputs do
      # all the inputs

      actions do
        action :submit
        cancel_link [:admin, :home_page]
      end
    end
  end

  controller do
    defaults singleton: true

    def resource
      @resource ||= HomePage.first
    end
  end
end

Our config/routes.rb:

YourApplicationName::Application.routes.draw do
  namespace :admin do
    resource :home_page, only: [:show, :edit, :update]
  end

  # ...

  ActiveAdmin.routes(self)

  # ...
end

Related Articles