Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move Multiple-Input Virtual Attributes to SimpleForm Custom Input Component

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).

like image 698
bookcasey Avatar asked Dec 09 '12 22:12

bookcasey


2 Answers

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.

like image 146
Jiří Pospíšil Avatar answered Oct 27 '22 12:10

Jiří Pospíšil


My humble opinion is that you would give better user experience if the user inputs the data in just one field . Here are my concerns :

Assuming you are using heights in limited range (probably human's height) , you can write a validation that detects what is the user input - inches or feet . Then you could make a validation link (or better a button ) asking if the input is what it meant to be (inches or feet detected) .

All this (including the dimension transformation while it's just inches -> feet) can be done in javascript , you can fetch the current dimensions by Ajax call and avoid reloading the whole code of the page .

EDIT : I've found an interesting point of view related with complicated inputs . Another useful resource about user interaction in filling form with feet and inches .

Your question is really interesting and I would love to see the solution you choose .

like image 29
R Milushev Avatar answered Oct 27 '22 11:10

R Milushev