lib/formtastic.rb in nofxx-formtastic-0.1.8 vs lib/formtastic.rb in nofxx-formtastic-0.1.9
- old
+ new
@@ -16,17 +16,17 @@
@@inline_errors = :sentence
@@label_str_method = :humanize
@@collection_label_methods = %w[to_label display_name full_name name title username login value to_s]
@@inline_order = [ :input, :hints, :errors ]
@@file_methods = [ :file?, :public_filename ]
+ @@priority_countries = ["Australia", "Canada", "United Kingdom", "United States"]
cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string,
:optional_string, :inline_errors, :label_str_method, :collection_label_methods,
- :inline_order, :file_methods
+ :inline_order, :file_methods, :priority_countries
# Keeps simple mappings in a hash
- #
INPUT_MAPPINGS = {
:string => :text_field,
:password => :password_field,
:numeric => :text_field,
:text => :text_area,
@@ -40,11 +40,11 @@
# and other factors (like the method name) to figure out what you probably want.
#
# Options:
#
# * :as - override the input type (eg force a :string to render as a :password field)
- # * :label - use something other than the method name as the label (or fieldset legend) text
+ # * :label - use something other than the method name as the label text, when false no label is printed
# * :required - specify if the column is required (true) or not (false)
# * :hint - provide some text to hint or help the user provide the correct information for a field
# * :input_html - provide options that will be passed down to the generated input
# * :wrapper_html - provide options that will be passed down to the li wrapper
#
@@ -53,20 +53,23 @@
# Most inputs map directly to one of ActiveRecord's column types by default (eg string_input),
# but there are a few special cases and some simplification (:integer, :float and :decimal
# columns all map to a single numeric_input, for example).
#
# * :select (a select menu for associations) - default to association names
+ # * :check_boxes (a set of check_box inputs for associations) - alternative to :select has_many and has_and_belongs_to_many associations
+ # * :radio (a set of radio inputs for associations) - alternative to :select belongs_to associations
# * :time_zone (a select menu with time zones)
- # * :radio (a set of radio inputs for associations) - default to association names
# * :password (a password input) - default for :string column types with 'password' in the method name
# * :text (a textarea) - default for :text column types
# * :date (a date select) - default for :date column types
# * :datetime (a date and time select) - default for :datetime and :timestamp column types
# * :time (a time select) - default for :time column types
# * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio)
# * :string (a text field) - default for :string column types
# * :numeric (a text field, like string) - default for :integer, :float and :decimal column types
+ # * :country (a select menu of country names) - requires a country_select plugin to be installed
+ # * :hidden (a hidden field) - creates a hidden field (added for compatibility)
#
# Example:
#
# <% semantic_form_for @employee do |form| %>
# <% form.inputs do -%>
@@ -76,11 +79,11 @@
# <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %>
# <% end %>
# <% end %>
#
def input(method, options = {})
- options[:required] = method_required?(method, options[:required])
+ options[:required] = method_required?(method) unless options.key?(:required)
options[:as] ||= default_input_type(method)
html_class = [ options[:as], (options[:required] ? :required : :optional) ]
html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors.on(method.to_s)
@@ -306,11 +309,70 @@
opts.merge!(:builder => Formtastic::SemanticFormBuilder)
args.push(opts)
fields_for(record_or_name_or_array, *args, &block)
end
+ # Generates the label for the input. It also accepts the same arguments as
+ # Rails label method. It has three options that are not supported by Rails
+ # label method:
#
+ # * :required - Appends an abbr tag if :required is true
+ # * :label - An alternative form to give the label content. Whenever label
+ # is false, a blank string is returned.
+ # * :as_span - When true returns a span tag with class label instead of a label element
+ #
+ # == Examples
+ #
+ # f.label :title # like in rails, except that it searches the label on I18n API too
+ #
+ # f.label :title, "Your post title"
+ # f.label :title, :label => "Your post title" # Added for formtastic API
+ #
+ # f.label :title, :required => true # Returns <label>Title<abbr title="required">*</abbr></label>
+ #
+ def label(method, options_or_text=nil, options=nil)
+ if options_or_text.is_a?(Hash)
+ return if options_or_text[:label] == false
+
+ options = options_or_text
+ text = options.delete(:label)
+ else
+ text = options_or_text
+ options ||= {}
+ end
+
+ text ||= humanized_attribute_name(method)
+ text << required_or_optional_string(options.delete(:required))
+
+ if options.delete(:as_span)
+ options[:class] ||= 'label'
+ template.content_tag(:span, text, options)
+ else
+ super(method, text, options)
+ end
+ end
+
+ # Generates error messages for the given method. Errors can be shown as list
+ # or as sentence. If :none is set, no error is shown.
+ #
+ # This method is also aliased as errors_on, so you can call on your custom
+ # inputs as well:
+ #
+ # semantic_form_for :post do |f|
+ # f.text_field(:body)
+ # f.errors_on(:body)
+ # end
+ #
+ def inline_errors_for(method, options=nil) #:nodoc:
+ return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors)
+
+ errors = @object.errors.on(method.to_s)
+ send("error_#{@@inline_errors}", Array(errors)) unless errors.blank?
+ end
+ alias :errors_on :inline_errors_for
+
+ #
# Stolen from Attribute_fu (http://github.com/giraffesoft/attribute_fu)
# Rails 2.3 Patches from http://github.com/odadata/attribute_fu
#
# Dinamically add and remove nested forms for a has_many relation.
#
@@ -452,13 +514,11 @@
# otherwise.
#
# * if the :required option isn't provided, and the plugin isn't available, the value of the
# configuration option @@all_fields_required_by_default is used.
#
- def method_required?(attribute, required_option) #:nodoc:
- return required_option unless required_option.nil?
-
+ def method_required?(attribute) #:nodoc:
if @object && @object.class.respond_to?(:reflect_on_all_validations)
attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
@object.class.reflect_on_all_validations.any? do |validation|
validation.macro == :validates_presence_of && validation.name == attribute_sym
@@ -470,19 +530,30 @@
# A method that deals with most of inputs (:string, :password, :file,
# :textarea and :numeric). :select, :radio, :boolean and :datetime inputs
# are not handled by this method, since they need more detailed approach.
#
- # If input_html is given as option, it's passed down to the input.
+ # If input_html is given as option, it's passed down to the input. The :size option is
+ # also passed in since it's so common to want to customize it.
#
def input_simple(type, method, options)
html_options = options.delete(:input_html) || {}
html_options = default_string_options(method).merge(html_options) if STRING_MAPPINGS.include?(type)
+ html_options[:size] = options.delete(:size) if options.has_key?(:size)
- input_label(method, options.delete(:label), options.slice(:required)) + send(INPUT_MAPPINGS[type], method, html_options)
+ self.label(method, options.slice(:label, :required)) +
+ self.send(INPUT_MAPPINGS[type], method, html_options)
end
+ # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
+ # Additionals options can be given and will be sent straight to hidden input
+ # element.
+ #
+ def hidden_input(method, options)
+ self.hidden_field(method, set_options(options))
+ end
+
# Outputs a label and a select box containing options from the parent
# (belongs_to, has_many, has_and_belongs_to_many) association. If an association
# is has_many or has_and_belongs_to_many the select box will be set as multi-select
# and size = 5
#
@@ -568,11 +639,11 @@
html_options[:multiple] ||= true
html_options[:size] ||= 5
end
input_name = generate_association_input_name(method)
- input_label(input_name, options.delete(:label), options.slice(:required)) +
+ self.label(input_name, options.slice(:label, :required)) +
self.select(input_name, collection, set_options(options), html_options)
end
alias :boolean_select_input :select_input
# Outputs a timezone select input as Rails' time_zone_select helper. You
@@ -583,11 +654,11 @@
# f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
#
def time_zone_input(method, options)
html_options = options.delete(:input_html) || {}
- input_label(method, options.delete(:label), options.slice(:required)) +
+ self.label(method, options.slice(:label, :required)) +
self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options)
end
# Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
# items, one for each possible choice in the belongs_to association. Each li contains a
@@ -612,11 +683,11 @@
# </fieldset>
#
# You can customize the options available in the set by passing in a collection (Array) of
# ActiveRecord objects through the :collection option. If not provided, the choices are found
# by inferring the parent's class name from the method name and simply calling find(:all) on
- # it (VehicleOwner.find(:all) in the example above).
+ # it (Author.find(:all) in the example above).
#
# Examples:
#
# f.input :author, :as => :radio, :collection => @authors
# f.input :author, :as => :radio, :collection => Author.find(:all)
@@ -730,54 +801,160 @@
time_inputs = [:hour, :minute]
time_inputs << [:second] if options[:include_seconds]
list_items_capture = ""
+ hidden_fields_capture = ""
# Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
datetime = @object ? @object.send(method) : nil
html_options = options.delete(:input_html) || {}
(inputs + time_inputs).each do |input|
html_id = generate_html_id(method, "#{position[input]}i")
field_name = "#{method}(#{position[input]}i)"
-
- list_items_capture << if options["discard_#{input}".intern]
+ if options["discard_#{input}".intern]
break if time_inputs.include?(input)
-
+
hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime
- template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id)
+ hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id)
else
opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name)
item_label_text = I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
- template.content_tag(:li,
+ list_items_capture << template.content_tag(:li,
template.content_tag(:label, item_label_text, :for => html_id) +
template.send("select_#{input}".intern, datetime, opts, html_options.merge(:id => html_id))
)
end
end
- field_set_and_list_wrapping_for_method(method, options, list_items_capture)
+ hidden_fields_capture + field_set_and_list_wrapping_for_method(method, options, list_items_capture)
end
+
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
+ # items, one for each possible choice in the belongs_to association. Each li contains a
+ # label and a check_box input.
+ #
+ # This is an alternative for has many and has and belongs to many associations.
+ #
+ # Example:
+ #
+ # f.input :author, :as => :check_boxes
+ #
+ # Output:
+ #
+ # <fieldset>
+ # <legend><span>Authors</span></legend>
+ # <ol>
+ # <li>
+ # <input type="hidden" name="book[author_id][1]" value="">
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
+ # </li>
+ # <li>
+ # <input type="hidden" name="book[author_id][2]" value="">
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
+ # </li>
+ # </ol>
+ # </fieldset>
+ #
+ # Notice that the value of the checkbox is the same as the id and the hidden
+ # field has empty value. You can override the hidden field value using the
+ # unchecked_value option.
+ #
+ # You can customize the options available in the set by passing in a collection (Array) of
+ # ActiveRecord objects through the :collection option. If not provided, the choices are found
+ # by inferring the parent's class name from the method name and simply calling find(:all) on
+ # it (Author.find(:all) in the example above).
+ #
+ # Examples:
+ #
+ # f.input :author, :as => :check_boxes, :collection => @authors
+ # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
+ # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
+ #
+ # You can also customize the text label inside each option tag, by naming the correct method
+ # (:full_name, :display_name, :account_number, etc) to call on each object in the collection
+ # by passing in the :label_method option. By default the :label_method is whichever element of
+ # Formtastic::SemanticFormBuilder.collection_label_methods is found first.
+ #
+ # Examples:
+ #
+ # f.input :author, :as => :check_boxes, :label_method => :full_name
+ # f.input :author, :as => :check_boxes, :label_method => :display_name
+ # f.input :author, :as => :check_boxes, :label_method => :to_s
+ # f.input :author, :as => :check_boxes, :label_method => :label
+ #
+ # You can set :value_as_class => true if you want that LI wrappers contains
+ # a class with the wrapped checkbox input value.
+ #
+ def check_boxes_input(method, options)
+ collection = find_collection_for_column(method, options)
+ html_options = options.delete(:input_html) || {}
+
+ input_name = generate_association_input_name(method)
+ value_as_class = options.delete(:value_as_class)
+ unchecked_value = options.delete(:unchecked_value) || ''
+ html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
+
+ list_item_content = collection.map do |c|
+ label = c.is_a?(Array) ? c.first : c
+ value = c.is_a?(Array) ? c.last : c
+
+ html_options.merge!(:id => generate_html_id(input_name, value.to_s.downcase))
+
+ li_content = template.content_tag(:label,
+ "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
+ :for => html_options[:id]
+ )
+
+ li_options = value_as_class ? { :class => value.to_s.downcase } : {}
+ template.content_tag(:li, li_content, li_options)
+ end
+
+ field_set_and_list_wrapping_for_method(method, options, list_item_content)
+ end
+
+
+ # Outputs a country select input, wrapping around a regular country_select helper.
+ # Rails doesn't come with a country_select helper by default any more, so you'll need to install
+ # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
+ # same way.
+ #
+ # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
+ #
+ # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
+ # which you can change to suit your market and user base (see README for more info on config).
+ #
+ # Examples:
+ # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
+ # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
+ #
+ def country_input(method, options)
+ raise "To use the :country input, please install a country_select plugin, like this one: http://github.com/rails/iso-3166-country-select" unless self.respond_to?(:country_select)
+
+ html_options = options.delete(:input_html) || {}
+ priority_countries = options.delete(:priority_countries) || @@priority_countries
+
+ self.label(method, options.slice(:label, :required)) +
+ self.country_select(method, priority_countries, set_options(options), html_options)
+ end
+
+
# Outputs a label containing a checkbox and the label text. The label defaults
# to the column name (method name) and can be altered with the :label option.
- #
- # Different from other inputs, :required options has no effect here and
# :checked_value and :unchecked_value options are also available.
#
def boolean_input(method, options)
html_options = options.delete(:input_html) || {}
input = self.check_box(method, set_options(options).merge(html_options),
options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
- # Generate the label by hand because required or optional does not make sense here
label = options.delete(:label) || humanized_attribute_name(method)
-
- self.label(method, input + label, options)
+ self.label(method, input + label, options.slice(:required))
end
# Generates an input for the given method using the type supplied with :as.
#
# If the input is included in INPUT_MAPPINGS, it uses input_simple
@@ -793,84 +970,50 @@
else
send("#{input_type}_input", method, options)
end
end
- # Generates error messages for the given method. Errors can be shown as list
- # or as sentence. If :none is set, no error is shown.
- #
- def inline_errors_for(method, options) #:nodoc:
- return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors)
-
- # Ruby 1.9: Strings are not Enumerable, ie no String#to_a
- errors = @object.errors.on(method.to_s)
- unless errors.respond_to?(:to_a)
- errors = [errors]
- else
- errors = errors.to_a
- end
-
- # Ruby 1.9: Strings are not Enumerable, ie no String#to_a
- errors = @object.errors.on(method.to_s)
- unless errors.respond_to?(:to_a)
- errors = [errors]
- else
- errors = errors.to_a
- end
-
- send("error_#{@@inline_errors}", errors) unless errors.empty?
- end
-
# Generates hints for the given method using the text supplied in :hint.
#
def inline_hints_for(method, options) #:nodoc:
return if options[:hint].blank?
template.content_tag(:p, options[:hint], :class => 'inline-hints')
end
# Creates an error sentence by calling to_sentence on the errors array.
#
def error_sentence(errors) #:nodoc:
- template.content_tag(:p, errors.to_sentence, :class => 'inline-errors')
+ template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
end
# Creates an error li list.
#
def error_list(errors) #:nodoc:
list_elements = []
errors.each do |error|
- list_elements << template.content_tag(:li, error)
+ list_elements << template.content_tag(:li, error.untaint)
end
template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
end
- # Generates the label for the input. Accepts the same options as Rails label
- # method and a fourth option that allows the label to be generated as span
- # with class label.
- #
- def input_label(method, text, options={}, as_span=false) #:nodoc:
- text ||= humanized_attribute_name(method)
- text << required_or_optional_string(options.delete(:required))
-
- if as_span
- options[:class] ||= 'label'
- template.content_tag(:span, text, options)
- else
- self.label(method, text, options)
- end
- end
-
# Generates the required or optional string. If the value set is a proc,
# it evaluates the proc first.
#
def required_or_optional_string(required) #:nodoc:
- string_or_proc = required ? @@required_string : @@optional_string
+ string_or_proc = case required
+ when true
+ @@required_string
+ when false
+ @@optional_string
+ else
+ required
+ end
- if string_or_proc.is_a? Proc
+ if string_or_proc.is_a?(Proc)
string_or_proc.call
else
- string_or_proc
+ string_or_proc.to_s
end
end
# Generates a fieldset and wraps the content in an ordered list. When working
# with nested attributes (in Rails 2.3), it allows %i as interpolation option
@@ -902,11 +1045,11 @@
# Also generates a fieldset and an ordered list but with label based in
# method. This methods is currently used by radio and datetime inputs.
#
def field_set_and_list_wrapping_for_method(method, options, contents)
template.content_tag(:fieldset,
- %{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} +
+ %{<legend>#{self.label(method, options.slice(:label, :required).merge!(:as_span => true))}</legend>} +
template.content_tag(:ol, contents)
)
end
# For methods that have a database column, take a best guess as to what the input method
@@ -922,14 +1065,15 @@
column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
if column
# handle the special cases where the column type doesn't map to an input method
- return :time_zone if column.type == :string && method.to_s =~ /time_?zone/
+ return :time_zone if column.type == :string && method.to_s =~ /time_zone/
return :select if column.type == :integer && method.to_s =~ /_id$/
return :datetime if column.type == :timestamp
return :numeric if [:integer, :float, :decimal].include?(column.type)
return :password if column.type == :string && method.to_s =~ /password/
+ return :country if column.type == :string && method.to_s =~ /country/
# otherwise assume the input name will be the same as the column type (eg string_input)
return column.type
else
obj = @object.send(method) if @object.respond_to?(method)