ActiveAdmin is a great framework for quickly creating attractive and powerful administrative interfaces for Ruby on Rails applications. It can handle all sorts of data fields, but for more complex database columns (such as PostgreSQL’s jsonb data type), it needs to be customized in to handle the data.
In the LIXY assessment platform we want to allow our customers to configure the behavior of our application for certain accounts and users to accommodate their specific needs.
There are several ways to accomplish this in Ruby on Rails, but I like using JSON data for this. Configurations are specific to the particular object that is being configured. When we use JSON as an attribute, it doesn’t require any additional database calls to get the configuration information.
We will begin by creating a database migration to add the jsonb column to the relevant table
bundle exec rails g migration AddConfigToOrganizations config:jsonb
NOTE: I recommend you not allow NULL values. I believe that in most cases (especially when dealing with JSON attributes) it is preferable to require a value. Unfortunately you can’t configure null or default values via the command line generator1, so you’ll need to modify the migration file. The final migration should look like:
class AddConfigToOrganizations < ActiveRecord::Migration[5.2]
def change
add_column :organizations, :config, :jsonb, null: false, default: {}
end
end
Next you’ll need to run the migration by executing: bundle exec rake db:migrate
,
which will apply the change to your database.
At this point, you can now view any existing organizations:
But if you try to create or edit an object, you will get a Formtastic::UnknownInputError
- Unable to find input class JsonbInput
exception, as ActiveAdmin - which uses
Formtastic to build it’s forms - doesn’t know what to do with a jsonb column:
Uh oh!
But don’t worry, there are a couple easy ways to solve this problem:
- Edit the JSON data directly
- Build a form that allows for editing the data, without needing to know the underlying structure
NOTE: Pick one option or the other, don’t do both.
Option 1:
If you don’t want / need well structured data, or will have only technical people that need to edit the data, you can simply use the activeadmin_json_editor gem, which will create a very pretty editing window for your JSON data:
The install / configuration instructions can be found on the gem’s page, but the code needed to get it to display on ActiveAdmin is straightforward:
# filename: app/admin/organizations.rb
permit_params :name, :config
form do |f|
f.inputs do
f.input :name
f.input :config, as: :jsonb
end
end
If that’s all you need, then you are done! You can now edit your JSON data right in Active Admin, pretty cool right?
Option 2:
For my use case we will have structured data since I will be using it to configure specific functionality. I’m designing it for people that are not very technical, so we need to be able to generate a much more user friendly form.
Since I know what my data will look like, I can create a form that allows the user to directly modify the specific configuration element they want, without having to worry about the structure of the data.
For example, this application is for building assessments, and each assessment has a set of steps, but not everybody uses the word “assessment”, or “step”. Maybe they call it a “test”, and “skill”, so we want to allow them to make those customizations. For this we’ll use a keys of “assessment_label”, and “skill_label” for the config param.
This is where things start to get a little tricky. Formtastic is great for working with Rails models, but it’s not designed to work with any arbitrary data structure.
First we’ll need to set up the params that ActiveAdmin accepts, and then tell formtastic how to present it:
permit_params :name, config: :assessment_label
form do |f|
f.inputs do
f.input :name, as: :string
f.inputs name: 'Config', for: :config do |g|
g.input :assessment_label,
require: false,
input_html: { value: organization.config['assessment_label'] }
g.input :skill_label,
require: false,
input_html: { value: organization.config['skill_label'] }
end
end
end
Now, it’s a little laborious to have to type organization.config['key_name']
for
every attribute we want to use. We can use rails’ store_accessor
2 3 to add
accessor methods to the model dry up our code:
# filename: app/models/organization.rb
class Organization < ApplicationRecord
validates :name, presence: true
store_accessor :config, :assessment_label, :skill_label
end
store_accessor
gives us direct access to the keys in the JSON attribute, allowing us
to change: input_html: { value: organization.config['skill_label'] }
to:
input_html: { value: organization.skill_label }
. Additionally, you no longer
need the extra attributes on the input.
If we have a lot of attributes we want to mange, we can get even more DRY and change our code to iterate over the array of attributes to generate the form inputs:
form do |f|
f.inputs do
f.input :name
f.inputs name: 'Config', for: :config do |g|
Organization.stored_attributes[:config].each do |accessor|
g.input accessor,
required: false,
input_html: { value: organization.send(accessor) }
end
end
end
f.actions
end
More Neat Tricks:
Storext
You can add type-constraints, and additional features by taking advantage of the storext gem. With storext you can define additional information on the attributes, such as type and defaults. Add `gem ‘storext’ to your Gemfile, and change your model to include Storext:
# filename: app/models/organization.rb
class Organization < ApplicationRecord
include Storext.model
validates :name, presence: true
store_attributes :config do
assessment_label String
skill_label String, default: 'Talent'
end
end
Form Display
One cool little thing I discovered while writing this was that the nesting of the
config portion of the form is optional, if you remove the |g|
from the inputs
block, the nesting goes away:
form do |f|
f.inputs do
f.input :name
f.inputs name: 'Config', for: :config do
Organization.stored_attributes[:config].each do |accessor|
f.input accessor,
required: false,
input_html: { value: organization.send(accessor) }
end
end
end
f.actions
end
This yields a form without nesting the config
fields:
You can also create the form without the ‘Config’ section at all:
form do |f|
f.inputs do
f.input :name
f.inputs for: :config do
Organization.stored_attributes[:config].each do |accessor|
f.input accessor,
required: false,
input_html: { value: organization.send(accessor) }
end
end
end
f.actions
end
Which creates the form with no separator:
Lastly, you can make the form appear as if the JSON keys are individual columns
in the table by omitting the inner form loop on the :config
attribute:
form do |f|
f.inputs do
f.input :name
Organization.stored_attributes[:config].each do |accessor|
f.input accessor
end
end
f.actions
end
or if you want (perhaps to present only a subset of the attributes), explicitly declaring the inputs the same way you would if they were table attributes:
form do |f|
f.inputs do
f.input :name
f.input :assessment_label
f.input :skill_label
end
f.actions
end
This generates a form that appears like the attributes exist are native ActiveModel attributes:
Requirements:
We assume you are using at least the following:
- Ruby on Rails 4.2+
- postgres 9.4+
- ActiveAdmin
NOTE: Since ActiveAdmin uses Formtastic for building it’s forms, you may be able to use the same approach to building forms for jsonb columns for Formtastic, and possibly adapt it withhout too much effort to other rails form builders