Move Multiple-Input Virtual Attributes to SimpleForm Custom Input Component

ruby-on-rails, simple-form

Solution

As far as I know, you can't really move anything but the rendering to custom input. SimpleForm doesn't get called once the form is submitted so it can't really interfere with the values in any way. I would love to be wrong about this as I needed it in the past also. Anyway, here's a version that keeps the conversion logic in the model.

The custom SimpleForm input:

# app/inputs/feet_and_inch_input.rb
class FeetAndInchInput < SimpleForm::Inputs::Base
  def input
    output           = ""
    label            = @options.fetch(:label) { @attribute_name.to_s.capitalize }

    feet_attribute   = "#{attribute_name}_feet".to_sym
    inches_attribute = "#{attribute_name}_inches".to_sym

    output << @builder.input(feet_attribute, wrapper: false, label: label)
    output << template.content_tag(:span, " feet ")
    output << @builder.input(inches_attribute, wrapper: false, label: false)
    output << template.content_tag(:span, " inches ")

    output.html_safe
  end

  def label
    ""
  end
end

The form. Note that I did not put the `<li>` tags inside the custom input, I think this way it's more flexible but feel free to change it.

# app/views/clients/_form.html.erb
<li>
  <%= f.input :height, as: :feet_and_inch %>
</li>

All of this relies on the fact that for every `height` attribute, you also have `height_feet` and `height_inches` attributes.

Now for the model, I am not honestly sure if this is the way to go, maybe someone might come up a better solution, BUT here it goes:

class Client < ActiveRecord::Base
  attr_accessible :name

  ["height", "weight"].each do |attribute|
    attr_accessible "#{attribute}_feet".to_sym
    attr_accessible "#{attribute}_inches".to_sym

    before_save do
      feet  = instance_variable_get("@#{attribute}_feet_ins_var").to_d
      inches = instance_variable_get("@#{attribute}_inches_ins_var").to_d

      self.send("#{attribute}=", feet*12 + inches)
    end

    define_method "#{attribute}_feet" do
      value = self.send(attribute)
      value.floor / 12 if value
    end

    define_method "#{attribute}_feet=" do |feet|
      instance_variable_set("@#{attribute}_feet_ins_var", feet)
    end

    define_method "#{attribute}_inches=" do |inches|
      instance_variable_set("@#{attribute}_inches_ins_var", inches)
    end

    define_method "#{attribute}_inches" do
      value = self.send(attribute)
      value % 12 if value && value % 12 != 0
    end
  end
end

It basically does the same but defines the methods dynamically. You can see at the top there's a list of attributes for which you want these methods to be generated.

Note that all of this is not really thoroughly tested and might kill your cat but hopefully can give you some ideas.

Problem

Height is stored in the database in inches. However feet and inches need their own individual inputs in the form: ``` Height: [_______] feet [_______] inches ``` So I used virtual attributes, and got it working. Here is a simplified version of my model: ``` class Client < ActiveRecord::Base attr_accessible :name, :height_feet, :height_inches before_save :combine_height def height_feet height.floor/12 if height end def height_feet=(feet) @feet = feet end def height_inches if height && height%12 != 0 height%12 end end def height_inches=(inches) #on save? @inches = inches end def combine_height self.height = @feet.to_d*12 + @inches.to_d #if @feet.present? end end ``` And the `_form` partial using simple_form: ``` <%= simple_form_for(@client) do |f| %> <ul> <%= f.error_notification %> <%= f.input :name %> <%= f.input :weight %> <li> <%= f.input :height_feet, :label => 'Height', :wrapper => false %> <span>feet</span> <%= f.input :height_inches, :label => false, :wrapper => false %> <span>inches</span> </li> <%= f.error :base %> </ul> <%= f.button :submit %> <% end %> ``` This works. But it is not ideal. I'd like to DRY this up and create a custom input component so I can add height to the form with `<%= f.input :height, as: :feet_and_inch %>`—and therefore any other input that follows the same pattern such as `<%= f.input :wingspan, as: :feet_and_inch %>`. I've experimented with custom components, but I can't get two inputs to display—and I'm not sure where is the best place to put the 'conversion' logic from feet and inches to inches (and likewise from inches back to feet and inches).

Original source