Search code examples
ruby-on-railsenumsactiveadminformtastic

ActiveAdmin and Formtastic: Displaying the value for radio buttons and select boxes using ENUM


I'm using ActiveAdmin to manage a large database and one of my models (ItemType) has an ENUM attribute (ItemType.units) and I am able to use Formtastic to render a select box and radio buttons like so:

f.input :unit, :as => :radio, :collection => ItemType.units, include_blank: false

The ENUM field is defined in the model like so:

class ItemType < ActiveRecord::Base
  enum unit: [ :Packages, :Pieces, :KG ]
end

This works correctly when creating a new resource, but the value is not retrieved when using the form to edit the same resource.


Here's the default "singular view" for the database record:

The singular view

And here's the default "edit view" for the same record. Notice how none of the values are selected:

The edit view


Solution

  • When you declare an enum and then access the mapping it returns a hash:

    ItemType.units #=> { "Packages" => 0, "Pieces" => 1, "KG" => 2 }
    

    When you are using it as a collection for your radio inputs it will set the value of the inputs to 0, 1, 2, etc. That value does not match the return value of ItemType#unit (as it will return the string representation of the enum such as "KG").

    Rails can only set the selected value when one of the values in the list matches the attribute's value. This will never happen in this case.

    This duality (string vs integer) will lead to an other pain point. You cannot actually save the form because you can only set the enum's value to one of the allowed values (string or integer representations).

    it = ItemType.new
    it.unit = "KG" # this works
    it.unit = :KG  # this works as well
    it.unit = 1    # this works but WTF?!
    it.unit = "1"  # this will raise an ArgumentError
    

    Since the form params are parsed into a string ActiveAdmin will try to assign "1" to ItemType#unit and it will fail.

    The solution is actually pretty simple. Use only the keys from the mapping:

    f.input :unit, :as => :radio, :collection => ItemType.units.keys
    

    Although if you can you should stay away from using AR's enums. A few reasons why:

    • it represent a meaningful string with a meaningless number (this means the data in the unit column will not make any sense without the application code)
    • it tightly couples the source of the application to the data (it would be hard to use the data by itself in an other application such as a db console)
    • the order of enum values has to be maintained (or an explicit mapping has to be provided). Neither of them are developer friendly.

    It is a better alternative to use a predefined string array and validate whether the value is in the list of predefined values. Something along the lines:

    class ItemType
      UNITS = %w[kg packages pieces]
      validates :unit, inclusion: { in: UNITS }
    end