Move Multiple-Input Virtual Attributes to SimpleForm Custom Input Component - ruby-on-rails

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

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.

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 .

Related

Ruby on Rails: How to automatically disable my out of stock products link, when stock == 0 on my product show view

I have an option on my products show page, that lets you choose the size that you would want your product in. When the product reaches stock == 0, it automatically says the Size name and "- sin stock (without available stock). The problem is that even if it displays without stock, you are still able to put the product with unavailable stock in the cart.
I would like to know if there is a ruby code or a way to see all the sizes and choose from them but have the ones without stock "xs - sin stock" disabled when clicked on.
What would be the best way to do this?
Size.rb file
class Size < ApplicationRecord
def self.options_for product_id
all.collect {|s| [display_format(s, product_id), s.id] }
end
private
def self.display_format size, product_id
name = '%-3.3s' % size.name
stock = Stock.by_size(product_id, size.id)
if stock == 0
**"#{name} - Sin Stock"**
else
"#{name}"
end
end
end
Part in Product show page where size is selected:
<%= label :size_id, 'Seleciona tu talle' %>
<%= form_tag product_items_path(product_id: #product.id) do %>
<%= select_tag :size_id,
options_for_select(
sizes_for(#product.id)),
prompt: "Seleciona tu talle"
%>
You can specify other html attributes after the first two elements in the array. This will disable the option if you have less than 1 available.
class Size < ApplicationRecord
def self.options_for product_id
all.collect do |size|
stock = Stock.by_size(product_id, size.id)
is_disabled = stock < 1
[display_format(size, stock), size.id, disabled: is_disabled]
end
end
private
def self.display_format size, stock
name = '%-3.3s' % size.name
if stock == 0
"#{name} - Sin Stock"
else
name
end
end
end
And using this in you view:
<%= form_tag product_items_path(product_id: #product.id) do %>
<%= label_tag :size_id, 'Seleciona tu talle' %>
<%= select_tag :size_id,
options_for_select(Size.options_for(#product.id), #product.size_id),
prompt: "Seleciona tu talle" %>
<% end %>
You would still want to check the stock level in the checkout process since I could manually modify the form information to submit bad data.

Formatting credit card number in a number_field_tag [duplicate]

I would like to make editing form fields as user-friendly as possible. For example, for numeric values, I would like the field to be displayed with commas (like number_with_precision).
This is easy enough on the display side, but what about editing? Is there a good way to do this?
I am using the Rails FormBuilder. Upon investigation, I found that it uses InstanceTag, which gets the values for fields by using <attribute>_value_before_type_cast which means overriding <attribute> won't get called.
The best I have come up with so far is something like this:
<%= f.text_field :my_attribute, :value => number_with_precision(f.object.my_attribute) %>
Or my_attribute could return the formatted value, like this:
def my_attribute
ApplicationController.helpers.number_with_precision(read_attribute(:my_attribute))
end
But you still have to use :value
<%= f.text_field :my_attribute, :value => f.object.my_attribute %>
This seems like a lot of work.
I prefer your first answer, with the formatting being done in the view. However, if you want to perform the formatting in the model, you can use wrapper methods for the getter and setter, and avoid having to use the :value option entirely.
You'd end up with something like this.
def my_attribute_string
foo_formatter(myattribute)
end
def my_attribute_string=(s)
# Parse "s" or do whatever you need to with it, then set your real attribute.
end
<%= f.text_field :my_attribute_string %>
Railscasts covered this with a Time object in a text_field in episode #32. The really clever part of this is how they handle validation errors. It's worth watching the episode for that alone.
This is an old question, but in case anyone comes across this you could use the number_to_X helpers. They have all of the attributes you could ever want for displaying your edit value:
<%= f.text_field :my_number, :value => number_to_human(f.object.my_number, :separator => '', :unit => '', :delimiter => '', :precision => 0) %>
There are more attributes available too: http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html
If you want a format to be created or maintained during editing, you will need to add Javascript to implement "masks." Here is a demo.
It was the first hit in these results.
You can use the number_format plugin. By specifying a number_format for an existing numeric attribute inside your model, the attribute will now appear as formatted to Rails in all forms and views. It will also be parsed back from that format (when assigned via forms) prior to insertion into the database. (The plugin also creates purely numeric unformatted_<attribute-name> accessors which can continue to be used for arithmetic, or for direct numerical assignment or retrieval by you for seamless integration.)
class MyModel < ActiveRecord::Base
# this model has the balance attribute, which we
# want to display using formatting in views,
# although it is stored as a numeric in the database
number_format :balance,
:precision => 2,
:delimiter => ',',
:strip_trailing_zeros => false
def increment_balance
unformatted_balance += 10
end
You can also combine the above with a Javascript solution, which can force the user to maintain the decimal point and thousands separators in place while editing, although this is really not necessary.
I have done something similar. We format times and lengths using a custom form builder. It makes use of the existing text_field, but wraps it so the value can be customized:
class SuperFormBuilder < ActionView::Helpers::FormBuilder
include ApplicationHelper
include FormHelper
include ActionView::Helpers::TagHelper
include ActionView::Helpers::FormTagHelper
def length_field(label,*args)
scale = 'medium'
args.each do |v|
if v.has_key?(:scale)
scale = v[:scale]
v.delete(:scale)
end
end
value = length_conversion(#object.send(label.to_sym),scale)
options = (args.length > 0) ? args.pop : {}
return has_error(label, text_field_tag(field_name(label),value,*args) + ' ' + length_unit(scale))
end
private
def field_name(label)
return #object_name + "[#{label}]"
end
def has_error(label, output)
return "<div class='fieldWithErrors'>#{output}</div>" if #object.errors[label]
return output
end
And it is used like this:
<%= form_for( #section, {:action => 'save', :id => #section.id}, :builder => SuperFormBuilder) do |sf| %>
<%= sf.length_field :feed_size_min_w, :size => 3, :scale => 'small' %>
<% end %>
The end result is a value in the appropriate unit based off their choice on system (Metric, Imperial) and scale IE small = inches or millimeters.
I basically copied the text_field method from the existing form builder, which uses the text_field_tag itself.
There are two gotchas: 1) Knowing the name of the object field and how to access the object to get the value which you want to format. 2) Getting the name right so when the form is submitted it is part of the correct params hash.
The form builder is given a class variable #object. You can get the value of the field using the .send method. In my case I send the label :feed_size_min_w to the #object and get its length back. I then convert it to my desired format, and give it to the text_field_tag.
The name of the field is key to having it end up in the params hash, in my instance the params[:sections] one. I made a little helper function called field_name that takes care of this.
Finally the has_error wraps the field in an error div if there are errors on that label.
I needed "nicer" format on some specified text fields, resolved it by adding this to my initializers. Seems to work nicely on Rails ~= 5.2 and it should be easy to customize.
class ActionView::Helpers::Tags::TextField
private
def value_before_type_cast # override method in ActionView::Helpers::Tags::Base
v = super
# format as you like, when you like
if #options.delete(:nice_decimal)
v = v.to_s.gsub('.', ',') if v.is_a?(BigDecimal)
end
v
end
end
Usage in form f
<%= f.text_field :foo, nice_decimal: true %>

Using two separate fields for the same parameter in a Rails form handler?

I'm new to Rails and am fixing a Rails 2 site. I have a form that lets the user add information for the starting location (:start) EITHER with an input OR with a dropdown field. However, I have found that when I include both options, only the dropdown (which comes last) submits data, while the input is ignored. What's the right way to include both options?
MY VIEW
<% form_for #newsavedmap, :html=>{:id=>'createaMap'} do |f| %>
<%= f.error_messages %>
<p>Enter a street address, city, and state:
<%= f.text_field :start, {:id=>"startinput", :size=>50}%></p>
<p>Or, select a location from the list:
<%= f.select :start, options_for_select(#itinerary.locations), {:include_blank => true }, {:id=>"startdrop"} %>
<input type="submit" id="savethismap" value="Save Map">
<% end %>
One way to achieve this is by using virtual attributes. Since both fields map to same attribute, you are going to have to pick which one to use.
# app/models/newsavedmap.rb
class Newsavedmap < ActiveRecord::Base
...
attr_accessible :start_text, :start_select
...
def start_text=(value)
#start_text = value if value
prepare_start
end
def start_select=(value)
#start_select = value if value
prepare_start
end
# start_text will fall back to self.start if #start_text is not set
def start_text
#start_text || self.start
end
# start_select will fall back to self.start if #start_select is not set
def start_select
#start_select || self.start
end
private
def prepare_start
# Pick one of the following or use however you see fit.
self.start = start_text if start_text
self.start = start_select if start_select
end
end
Then your form needs to use the virtual attributes:
<%= f.text_field :start_text, {:id=>"startinput", :size=>50}%></p>
<p>Or, select a location from the list:
<%= f.select :start_select, options_for_select(#itinerary.locations), {:include_blank => true }, {:id=>"startdrop"} %>
Other options are:
Use text_field as the primary and update it's value with selected option if user selects an option.
Add a hidden field in your form and use JavaScript to update the hidden field's value when text_field text gets updated or select option changes

multiple text fields for a single database entry rails 3

In the app I am building I have a need to combine multiple text fields into a single database column.
For example my "Business" entry has a column "Discount"
The text field I want to read something like this:
<%= f.text_field :discount %> % Off <%= f.text_field :discount %>.
I want both of these to be entered into the database as a string: "10% Off Shoes" (or whatever).
Is there a way to do this in Rails 3?
Thanks!
**Edit!
I tried Pan Thomakos's solution (using virtual attributes) and now I am getting the following error:
You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.split
Extracted source (around line #3):
1:
2: <%= f.label :cost %><br />
3: <%= f.text_field :percentage %> % Off <%= f.text_field :product %>.
app/models/business.rb:11:in `percentage'
I'm not really sure how to handle this! Admittedly I am weak when it comes to working within the model, I probably would have handled this in the controller.
Thanks!
Yes, the best way to do it is to use virtual attributes. Each virtual attribute will keep track of the different parts of the discount and the discount will be the combined field. Here is how I would implement it:
class Business
attr_writer :percentage, :product
before_save :create_discount
def percentage
#percentage.nil? ? discount.to_s.split('% Off ').first : #percentage
end
def product
#product.nil? ? discount.to_s.split('% Off ').last : #product
end
protected
def create_discount
discount = "#{#percentage}% Off #{#product}" unless #product.nil? || #percentage.nil?
end
end
You can then modify your view to:
<%= f.text_field :percentage %> % Off <%= f.text_field :product %>.
Switch the logic around.
class Business
attr_writer :percentage, :product
before_save :create_discount
def percentage
#percentage.nil? ? #percentage : discount.to_s.split('% Off ').first
end
def product
#product.nil? ? #product : discount.to_s.split('% Off ').last
end
protected
def create_discount
discount = "#{#percentage}% Off #{#product}" unless #product.nil? || #percentage.nil?
end
end

How do I add 'required' to form label if specific option is set?

Using Rails, I would like to add: <span class='req'>•</span> within my <label> tags in my views, if I set an option on the label form helper; i.e.
<%= f.label :address_1, "Address 2:", :req => true -%>
should produce:
<label for="model_address_1"><span class='req'>•</span> Address 1:</label>
rather than:
<label for="model_address_1" req="true">Address 1:</label>
as it does now. I realise I may have to override the default form builder or create my own form builder, but don't know how to do this. Any suggestions? Thanks in advance!
Update: I'm doing this in an attempt to DRY up my code, and I realise that I could just paste the span snippet above into all the labels in all of my views but I'd like to avoid that.
This is untested, but something along these lines should work.
# in lib/labeled_form_builder.rb
class LabeledFormBuilder < ActionView::Helpers::FormBuilder
def label(field_name, label = nil, options = {})
if options.delete(:req)
label ||= field_name.to_s.humanize
label = #template.content_tag(:span, "•", :class => "req") + " " + label
end
super(field_name, label, options)
end
end
And then use that builder in your view
<%= form_for #item, :builder => LabeledFormBuilder do |f| %>
You might be interested in Episode 3 of my Mastering Rails Forms screencast series where I go into detail on creating form builders.
Small change to ryanb's to get it to work in Rails 3. You just need to tell content_tag to not escape the bullet.
class LabeledFormBuilder < ActionView::Helpers::FormBuilder
def label(field_name, options = {}, label = nil)
if options.delete(:req)
label ||= field_name.to_s.humanize
label = #template.content_tag(:span, "•", {:class => "req"}, false) + " " + label
end
super(field_name, label, options)
end
end

Resources