Website logo

Tag: Formtastic

The Formtastic ruby gem

Working with JSONB in ActiveAdmin

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 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 better 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 this:

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:

Display organization object details

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 its forms – doesn’t know what to do with a jsonb column:

JsonbInput exception details

Uh oh!

But don’t worry, there are a couple of easy ways to solve this problem:

  1. Edit the JSON data directly
  2. 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
Fancy json editor

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 the 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
jsonb form fields

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_accessor23 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 manage, we can get even more DRY in our code and change it 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:

jsonb form fields with no nesting

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:

form fields with no label 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:

Native appearance form fields

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 its forms, you may be able to use the same approach to building forms for jsonb columns for Formtastic, and possibly adapt it without too much effort to other rails form builders

Afam Agbodike