How to Handle Singletons in ActiveAdmin
Ryan Stenberg, Former Developer
Article Category:
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
toshow
). - 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