module Transit
module Builders
##
# Base class for creating form builders
#
class FormBuilder < ActionView::Helpers::FormBuilder #:nodoc:
# Access the template object
attr_accessor :template
# Tracks the order in which fields are used in the form, this allows the easy rebuilding of the submitted form
# data when submitted since normally the hash isn't ordered.
attr_accessor :field_order
# Tracks the field currently being "processed"
attr_accessor :current_field_type
# create overrides for custom rendering
[:email_field, :password_field, :text_field, :text_area, :url_field].each do |method|
class_eval <<-FUNC, __FILE__, __LINE__ + 1
alias :_super_#{method} :#{method}
def #{method}(method_name, *args)
render_field_as_custom(:#{method}, method_name, *args)
end
FUNC
end
##
# Modified label tag to support adding a 'required' asterisk to the end of the label.
# Same params as the original implementation
#
def label(method, text = nil, options = {}, &block) #:nodoc:
options, text = text, nil if text.is_a?(Hash)
text ||= method.to_s.humanize
options.stringify_keys!
klasses = (options.delete(['class']) || "").split(" ")
klasses << 'field_with_errors' if errors_on_attribute?(method)
options['class'] = klasses.join(" ") unless klasses.compact.empty?
text = "#{text} *".html_safe if attribute_required?(method) || required_by_option?(options.delete('required'))
super(method, text, options, &block)
end
##
#
# Creates a button tag to be used in a form instead of the default input[type=submit]
# to help make CSS styling easier
#
# @param [String] value The text for the button
# @param [Hash] options HTML options to be passed to the button
# @option [String] icon If included, adds an image to the button to be used as an icon
#
def button(value = nil, options = {})
value, options = nil, value if value.is_a?(Hash)
value ||= submit_default_value
value = [image_tag(icon, :class => 'icon'), value].join(' ') if icon = options.delete(:icon)
klasses = (options.delete(:class) || "").split(" ")
klasses << "button"
options['class'] = klasses.join(" ")
content_tag(:button, value.to_s.html_safe, options.reverse_merge!({ "type" => "submit", "name" => "commit" }))
end
# Overrides fields_for to make sure the form builder is set properly
#
def fields_for(record_or_name_or_array, *args, &block) #:nodoc:
opts = args.extract_options!
opts.merge!(:builder => Transit::Builders::FormBuilder)
args.push(opts)
super(record_or_name_or_array, *args, &block)
end
##
#
# Generate a select tag with the 50 US states as options
#
# @param [Symbol] method The object method/attribute to be used
# @param [Hash] options Same as Rails' select options hash
# @option options [Symbol] :international Include an international option
# @option options [Symbol] :abbreviate Use an abbreviated version of the state name for the value
# @param [Hash] html_options Same as Rails' html_options hash
#
# @return [String] HTML select tag
#
def state_select(method, options = {}, html_options = {})
abbr = options.delete(:abbreviate)
abbr = !(abbr.nil? || abbr === false)
select(method, @template.options_for_select(options_for_state_select(abbr, options.delete(:international)), @object.try(:state)), options, html_options)
end
protected
##
#
# Returns a list of US states as an array
#
# @param [Boolean] abbreviate Abbreviate the value
# @param [Boolean, String] incl_international Include an additional state for "International"
#
# @return [Array] An array of states
#
def options_for_state_select(abbreviate = false, incl_international = false)
incl_international ||= false
state_list = [
['Alabama', "AL"],['Alaska', "AK"],['Arizona', "AZ"],['Arkansas', "AR"],['California', "CA"],['Colorado', "CO"],
['Connecticut', "CT"],['District of Columbia', "DC"],['Delaware', "DE"],['Florida', "FL"],['Georgia', "GA"],
['Hawaii', "HI"],['Idaho', "ID"],['Illinois', "IL"],['Indiana', "IN"],['Iowa', "IA"],['Kansas', "KS"],['Kentucky', "KY"],
['Louisiana', "LA"],['Maine', "ME"],['Maryland', "MD"],['Massachusetts', "MA"],['Michigan', "MI"],['Minnesota', "MN"],
['Mississippi', "MS"],['Missouri', "MO"],['Montana', "MT"],['Nebraska', "NE"],['Nevada', "NV"],['New Hampshire', "NH"],
['New Jersey', "NJ"],['New Mexico', "NM"],['New York', "NY"],['North Carolina', "NC"],['North Dakota', "ND"],
['Ohio', "OH"],['Oklahoma', "OK"],['Oregon', "OR"],['Pennsylvania', "PA"],['Rhode Island', "RI"],['South Carolina', "SC"],
['South Dakota', "SD"],['Tennessee', "TN"],['Texas', "TX"],['Utah', "UT"],['Vermont', "VT"],['Virginia', "VA"],['Washington', "WA"],
['West Virginia', "WV"],['Wisconsin', "WI"],['Wyoming', "WY"]
].map do |state|
(abbreviate ? state : [state.first, state.first])
end
state_list << ['International', incl_international] unless incl_international === false
state_list
end
##
#
# Checks to see if a particular attribute is required, if so, a "required" attribute is added to the field.
#
# @param [Symbol] attribute The attribute to check against validators
# @return [Boolean]
#
def attribute_required?(attribute)
validates_presence?(attribute) || validates_inclusion?(attribute)
end
# Convenience method to use the +content_tag+ method from our template
#
def content_tag(tag, content, options = {}, escape = true, &block) #:nodoc:
@template.content_tag(tag, content, options, escape, &block)
end
##
# Checks to see if there are errors for the particular method or attribute
#
# @param [Symbol] method The method/attribute to check
#
# @return [Boolean]
#
def errors_on_attribute?(method)
return false if @object.nil?
!(@object.errors.empty? || !@object.errors[method.to_sym].present? || [@object.errors[method.to_sym]].flatten.empty?)
end
##
#
# Checks a passed validator to see if it is required
# 'borrowed' from Formtastic by Justin French (see https://github.com/justinfrench/formtastic)
#
# @param [Hash] options Validator options
#
def options_require_validation?(options)
allow_blank = options[:allow_blank]
return !allow_blank unless allow_blank.nil?
if_condition = !options[:if].nil?
condition = if_condition ? options[:if] : options[:unless]
condition = if condition.respond_to?(:call)
condition.call(@object)
elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
@object.send(condition)
else
condition
end
if_condition ? !!condition : !condition
end
##
#
# Checks an options hash to determine if a required method/attribute was overridden manually
# @param [Hash] options The options hash to check
#
#
def required_by_option?(options)
req = (options.is_a?(Hash) ? options.stringify_keys[:required] : options)
!(req.to_s === 'false' || req.nil?)
end
##
#
# Wrapper method used by all form fields to customize the output
#
# @param [Symbol] helper_method Original Rails helper method name
# @param [Symbol] method Object method / symbol used for the element
# @param [Array] args Array of original arguments passed to the helper
#
# @return [String] Rendered HTML tag for the element
#
def render_field_as_custom(helper_method, method, *args)
@current_field_type = helper_method
options = args.extract_options!
(@field_order ||= []) << method
# Add an error class to the field if it has errors
#
if errors_on_attribute?(method)
klasses = (options.delete(:class) || "").split(" ")
klasses << "field_with_errors"
options[:class] = klasses.join(" ")
end
# Add a required attribute to the field if it is required
# Skip if false was passed as the required option
#
options[:required] = "required" if attribute_required?(method) && options.delete(:required).to_s != 'false'
options['data-validates-uniqueness'] = "true" if validates_uniqueness?(method)
result = send(:"_super_#{helper_method}", method, *(args << options))
messages = @object.nil? ? [] : @object.errors[method]
render_field_with_errors(method, result, messages)
end
##
#
# Renders the passed +html_tag+ with the custom error_template
#
# @param [Symbol] method The method/attribute to check for errors
# @param [Object, String] html_tag Instance of an input tag or a string
# @param [Array] messages An array of all error messages to be added to the template
#
def render_field_with_errors(method, html_tag, messages)
return html_tag unless errors_on_attribute?(method)
error_class = 'field_with_errors'
message_error_class = 'errors_for_field'
render_binding = binding
error_template = %{
<%= html_tag %>
<%= [messages].flatten.join(",") %>
}
renderer = ERB.new(error_template)
renderer.result(render_binding).to_s.html_safe
end
##
#
# Compiles an array of all validators for a particular attribute
# @param [Symbol] attribtue The attribute to check
#
def validators_for(attribute)
return [] if @object.nil?
return [] unless @object.class.respond_to?(:validators_on)
attribute = attribute.to_s.sub(/_id$/, '').to_sym
@object.class.validators_on(attribute).uniq
end
##
#
# Convenience method to see if a particular attribute has validators
# @param [Symbol] attribute The attribute to check
#
def validators_for?(attribute)
!validators_for(attribute).empty?
end
##
#
# Checks for a presence validation on a particular attribute
# @param [Symbol] attribute The attribute to check
#
def validates_presence?(attribute)
validator_of_type_exists?(validators_for(attribute), :presence)
end
##
#
# Checks for a uniqueness validation on a particular attribute
# @param [Symbol] attribute The attribute to check
#
def validates_uniqueness?(attribute)
validator_of_type_exists?(validators_for(attribute), :uniqueness, false)
end
##
#
# Checks for inclusion validation on a particular attribute
# @param [Symbol] attribute The attribute to check
#
def validates_inclusion?(attribute)
validator_of_type_exists?(validators_for(attribute), :inclusion)
end
private
def validator_of_type_exists?(validators, kind, check_options = true) #:nodoc: @private
validators.detect do |validator|
exists = (validator.kind.to_s == kind.to_s)
next exists unless (check_options && exists) && validator.options.present?
options_require_validation?(validator.options)
end
end
end
end
end