simple_form icon indicating copy to clipboard operation
simple_form copied to clipboard

The state of enums in simple_form

Open emilebosch opened this issue 4 years ago • 3 comments

Hi all,

Out of curiosity, over the years, I've aways had to type extra code to let the enums work in simple form. Why is this still the case? Basically any other fields just works out of the box. Is there here a reason I am totally missing?

When I think simple form I think simplicity, no need to worry, and no typing too much. It just works for all other types except enums. Any reason why?

emilebosch avatar Aug 21 '19 14:08 emilebosch

Here's what I'm using for enum in case it helps someone else - it seems to make everything work. If code is not interested in being changed, we can at least add this to the readme to make utilizing enums easy?

model/user.rb:

class User < ApplicationRecord
  ..
  enum user_type: { viewer: 0, editor: 1, admin: 2 }
end

view/users/_form.rb:

<%= f.input :user_type, collection: User.user_types.keys, selected: @user.user_type %>

jonmchan avatar Aug 04 '21 03:08 jonmchan

Rails 7.1 now allows you to validate enum columns, which allows a bit more fine grained control of when to show blank options - https://api.rubyonrails.org/classes/ActiveRecord/Enum.html.

We've wrapped the CollectionSelectInput to ease the creation of enum drop downs to something like <%= f.input :user_type %>, setting the allow_blank & prompt settings as best we can, not sure if it is useful to others:

class EnumInput < SimpleForm::Inputs::CollectionSelectInput
  def initialize(builder, attribute_name, column, input_type, options = {})
    raise ArgumentError, "EnumInput requires an enum column." unless column.is_a? ActiveRecord::Enum::EnumType

    # Enum's are only required if we do not allow nil values
    inclusion_validator = builder.object.class.validators_on(attribute_name).find { |v| v.kind == :inclusion }
    options[:required] = inclusion_validator && !inclusion_validator&.options&.dig(:allow_nil)

    # If a prompt & include_blank are both present, we'll show 2 options before our enum values
    # priority is given to the prompt, so we'll remove the include_blank option
    #
    # If our enum is required, we remove the include_blank option (can't be nil)
    # This lets SimpleForm include it for new fields, and exclude for preset fields
    #
    # Otherwise we'll show a blank option before our enum values
    if options[:prompt].present? || options[:required]
      options.delete(:include_blank)
    else
      options[:include_blank] = true
    end

    super
  end
  def collection
    @collection ||= begin
      raise ArgumentError, "Collections are inferred when using the enum input, custom collections are not allowed." if options.key?(:collection)
      object.defined_enums[attribute_name.to_s].keys.map do |key|
        [object.class.human_enum_name(attribute_name, key), key] # Our i18n translations aren't fully standard so this may not apply to everyone. WIP.
      end
    end
  end
end

And rails initializer to register the type:

module RegisterEnumAsSimpleFormDefaultType
  def default_input_type(attribute_name, column, options)
    # If we are explicit about the type, use that
    return options[:as].to_sym if options[:as]

    if column.is_a? ActiveRecord::Enum::EnumType
      # If we are using an enum, use our custom EnumInput
      :enum
    else
      # Otherwise, use the default simple form type lookup
      super
    end
  end
end

# Ensure we prepend this module so it is called before the default lookup
SimpleForm::FormBuilder.prepend(RegisterEnumAsSimpleFormDefaultType)

tvongaza avatar Oct 11 '23 02:10 tvongaza

Thanks @tvongaza!

If anyone else wants to use this, you may need to change the class definition to this:

class EnumInput < SimpleForm::Inputs::CollectionSelectInput

I replaced the collection function with this:

def collection
    @collection ||= begin
      raise ArgumentError,
        "Collections are inferred when using the enum input, custom collections are not allowed." if options.key?(:collection)

      object.defined_enums[attribute_name.to_s].keys.map do |key|
        [key.to_s.capitalize, key]
      end
    end
end

rhulse avatar Dec 11 '23 22:12 rhulse