lib/formtastic.rb in ShadowBelmolve-formtastic-0.1.6 vs lib/formtastic.rb in ShadowBelmolve-formtastic-0.2.1
- old
+ new
@@ -16,17 +16,22 @@
@@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"]
+ @@i18n_lookups_by_default = false
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, :i18n_lookups_by_default
+ I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}',
+ '{{model}}.{{attribute}}',
+ '{{attribute}}']
+
# Keeps simple mappings in a hash
- #
INPUT_MAPPINGS = {
:string => :text_field,
:password => :password_field,
:numeric => :text_field,
:text => :text_area,
@@ -40,32 +45,36 @@
# 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
#
# Input Types:
#
# 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 -%>
@@ -75,33 +84,34 @@
# <%= 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)
- options[:label] ||= if @object && @object.class.respond_to?(:human_attribute_name)
- @object.class.human_attribute_name(method.to_s)
- else
- method.to_s.send(@@label_str_method)
- end
+ html_class = [ options[:as], (options[:required] ? :required : :optional) ]
+ html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors[method.to_sym]
+ wrapper_html = options.delete(:wrapper_html) || {}
+ wrapper_html[:id] ||= generate_html_id(method)
+ wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
+
if [:boolean_select, :boolean_radio].include?(options[:as])
::ActiveSupport::Deprecation.warn(":as => :#{options[:as]} is deprecated, use :as => :#{options[:as].to_s[8..-1]} instead", caller[3..-1])
end
- html_class = [ options[:as], (options[:required] ? :required : :optional) ].join(' ')
- html_class << ' error' if @object && @object.respond_to?(:errors) && @object.errors.on(method.to_s)
+ if options[:input_html] && options[:input_html][:id]
+ options[:label_html] ||= {}
+ options[:label_html][:for] ||= options[:input_html][:id]
+ end
- html_id = generate_html_id(method)
-
list_item_content = @@inline_order.map do |type|
send(:"inline_#{type}_for", method, options)
end.compact.join("\n")
- return template.content_tag(:li, list_item_content, { :id => html_id, :class => html_class })
+ return template.content_tag(:li, list_item_content, wrapper_html)
end
# Creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be
# called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc),
# or with a list of fields. These two examples are functionally equivalent:
@@ -239,11 +249,11 @@
field_set_and_list_wrapping(html_options, &block)
else
if @object && args.empty?
args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to }
args += @object.class.content_columns.map(&:name)
- args -= %w[created_at updated_at created_on updated_on]
+ args -= %w[created_at updated_at created_on updated_on lock_version]
args.compact!
end
contents = args.map { |method| input(method.to_sym) }
field_set_and_list_wrapping(html_options, contents)
@@ -277,12 +287,18 @@
#
# The value of the button text can be overridden:
#
# <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
#
- def commit_button(value=nil, options={})
- value ||= save_or_create_button_text
+ # And you can pass html atributes down to the input, with or without the button text:
+ #
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
+ # <%= form.commit_button :class => "pretty" %> => <input name="commit" type="submit" value="Save Post" class="pretty" />
+
+ def commit_button(*args)
+ value = args.first.is_a?(String) ? args.shift : save_or_create_button_text
+ options = args.shift || {}
button_html = options.delete(:button_html) || {}
template.content_tag(:li, self.submit(value, button_html), :class => "commit")
end
# A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
@@ -309,11 +325,73 @@
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
+ # * :input_name - Gives the input to match for. This is needed when you want to
+ # to call f.label :authors but it should match :author_ids.
+ #
+ # == 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 = localized_attribute_string(method, text, :label) || humanized_attribute_name(method)
+ text += required_or_optional_string(options.delete(:required))
+
+ input_name = options.delete(:input_name) || method
+ if options.delete(:as_span)
+ options[:class] ||= 'label'
+ template.content_tag(:span, text, options)
+ else
+ super(input_name, 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[method.to_sym]
+ 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.
#
@@ -497,22 +575,25 @@
end
output.join
end
end
- private
+ protected
def association_name(class_name)
@object.respond_to?("#{class_name}_attributes=") ? class_name : class_name.pluralize
end
def extract_option_or_class_name(hash, option, object)
(hash.delete(option) || object.class.name.split('::').last.underscore)
end
- # End attribute_fu magic #
- protected
+ # Prepare options to be sent to label
+ #
+ def options_for_label(options)
+ options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
+ end
# Deals with :for option when it's supplied to inputs methods. Additional
# options to be passed down to :for should be supplied using :for_options
# key.
#
@@ -569,13 +650,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
@@ -591,15 +670,25 @@
#
# If input_html is given as option, it's passed down to the input.
#
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 = default_string_options(method, type).merge(html_options) if STRING_MAPPINGS.include?(type)
- input_label(method, options.delete(:label), options.slice(:required)) + send(INPUT_MAPPINGS[type], method, html_options)
+ self.label(method, options_for_label(options)) +
+ 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
#
@@ -607,46 +696,50 @@
#
# f.input :author
#
# <label for="book_author_id">Author</label>
# <select id="book_author_id" name="book[author_id]">
+ # <option value=""></option>
# <option value="1">Justin French</option>
# <option value="2">Jane Doe</option>
# </select>
#
# Example (has_many):
#
# f.input :chapters
#
# <label for="book_chapter_ids">Chapters</label>
# <select id="book_chapter_ids" name="book[chapter_ids]">
+ # <option value=""></option>
# <option value="1">Chapter 1</option>
# <option value="2">Chapter 2</option>
# </select>
#
# Example (has_and_belongs_to_many):
#
# f.input :authors
#
# <label for="book_author_ids">Authors</label>
# <select id="book_author_ids" name="book[author_ids]">
+ # <option value=""></option>
# <option value="1">Justin French</option>
# <option value="2">Jane Doe</option>
# </select>
#
#
- # You can customize the options available in the select 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).
+ # You can customize the options available in the select by passing in a collection (an Array or
+ # Hash) 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).
#
# Examples:
#
# f.input :author, :collection => @authors
# f.input :author, :collection => Author.find(:all)
# f.input :author, :collection => [@justin, @kate]
# f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
#
# Note: This input looks for a label method in the parent association.
#
# You can 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
@@ -674,33 +767,46 @@
#
# Examples:
#
# f.input :authors, :input_html => {:size => 20, :multiple => true}
#
+ # By default, all select inputs will have a blank option at the top of the list. You can add
+ # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
+ #
def select_input(method, options)
collection = find_collection_for_column(method, options)
html_options = options.delete(:input_html) || {}
+ unless options.key?(:include_blank) || options.key?(:prompt)
+ options[:include_blank] = true
+ end
+
reflection = find_reflection(method)
if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
+ options[:include_blank] = false
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(method, options_for_label(options).merge(:input_name => input_name)) +
self.select(input_name, collection, set_options(options), html_options)
end
alias :boolean_select_input :select_input
- # Output ...
+ # Outputs a timezone select input as Rails' time_zone_select helper. You
+ # can give priority zones as option.
+ #
+ # Examples:
+ #
+ # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
+ #
def time_zone_input(method, options)
html_options = options.delete(:input_html) || {}
- input_name = generate_association_input_name(method)
- input_label(input_name, options.delete(:label), options.slice(:required)) +
- self.time_zone_select(input_name, options.delete(:priority_zones), set_options(options), html_options)
+ self.label(method, options_for_label(options)) +
+ 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
# label and a radio input.
@@ -721,20 +827,21 @@
# <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
# </li>
# </ol>
# </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).
+ # You can customize the options available in the select by passing in a collection (an Array or
+ # Hash) 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 => :radio, :collection => @authors
# f.input :author, :as => :radio, :collection => Author.find(:all)
# f.input :author, :as => :radio, :collection => [@justin, @kate]
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
#
# 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.
@@ -771,13 +878,10 @@
field_set_and_list_wrapping_for_method(method, options, list_item_content)
end
alias :boolean_radio_input :radio_input
-
-
-
# Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
# items (li), one for each fragment for the date (year, month, day). Each li contains a label
# (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
#
# Some of Rails' options for select_date are supported, but not everything yet.
@@ -845,123 +949,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
- # Add a list of checkboxes, like radio
- #
- # Example:
- # Link
- # belongs_to :author
- # belongs_to :post
- #
- # Author
- # has_many :links
- # has_many :posts, :through => :links
- #
- # Post
- # has_many :links
- # has_many :authors, :through => :links
- #
- # # posts/new.html.erb
- # <% semantic_form_for @post do |post| %>
- # <%= post.input :title %>
- # <h3>This post belongs to<h3>
- # <%= post.input :author, :as => :check_boxes %>
- # <% end %>
- #
- # # Output
- # ...
- # <li class="check_boxes required" id="post_authors_input">
- # <fieldset><legend><span class="label">Authors</span></legend>
- # <ol><li>
- # <label for="post_author_ids_1">
- # <input name="post[author_ids][]" type="hidden" value="" />
- # <input checked="checked" id="post_author_ids" name="post[author_ids][]"
- # type="checkbox" value="1" /> Renan T. Fernandes
- # </label>
- # </li>
- # ....
- # </ol>
- # </fieldset>
- # </li>
- # ....
- #
- # NOTE : Accept all radio options, plus :checked_value and :unchecked_value
- # NOTEĀ²: Don't change :(un)checkd_value unless you know what you're doing !
- #
+
+ # 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 = set_options(options).merge(options.delete(:input_html) || {})
+ collection = find_collection_for_column(method, options)
+ html_options = options.delete(:input_html) || {}
- input_name = generate_association_input_name(method)
- html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
- value_as_class = options.delete(:value_as_class)
+ 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
+ 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,
- options.delete(:checked_value) || value, options.delete(:unchecked_value) || '')
- } #{label}", :for => generate_html_id(input_name, value.to_s.downcase)
+ "#{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
- alias :checkboxes_input :check_boxes_input
- alias :check_box_input :check_boxes_input
- alias :checkbox_input :check_boxes_input
+
+
+ # 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_for_label(options)) +
+ 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) || {}
- content = self.check_box(method, set_options(options).merge(html_options),
- options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
+ input = self.check_box(method, set_options(options).merge(html_options),
+ options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
- # required does not make sense in check box
- input_label(method, content + options.delete(:label), :skip_required => true)
+ label = options.delete(:label) || humanized_attribute_name(method)
+ self.label(method, input + label, options_for_label(options))
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
@@ -977,74 +1118,51 @@
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
-
- 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:
- options[:hint].blank? ? '' : template.content_tag(:p, options[:hint], :class => 'inline-hints')
+ options[:hint] = localized_attribute_string(method, options[:hint], :hint)
+ 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 << required_or_optional_string(options.delete(:required)) unless options.delete(:skip_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
@@ -1075,45 +1193,48 @@
# 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)
+ contents = contents.join if contents.respond_to?(:join)
+
template.content_tag(:fieldset,
- %{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} +
+ %{<legend>#{self.label(method, options_for_label(options).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 inout method
+ # For methods that have a database column, take a best guess as to what the input method
# should be. In most cases, it will just return the column type (eg :string), but for special
# cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
# something different (like :password and :select).
#
# If there is no column for the method (eg "virtual columns" with an attr_accessor), the
# default is a :string, a similar behaviour to Rails' scaffolding.
#
def default_input_type(method) #:nodoc:
- return :string if @object.nil?
-
- # Find the column object by attribute
column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
- # Associations map by default to a select
- return :select if column.nil? && find_reflection(method)
-
if column
# handle the special cases where the column type doesn't map to an input method
- 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 :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)
+ if @object
+ return :select if find_reflection(method)
- return :file if obj && @@file_methods.any? { |m| obj.respond_to?(m) }
+ file = @object.send(method) if @object.respond_to?(method)
+ return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
+ end
+
return :password if method.to_s =~ /password/
return :string
end
end
@@ -1171,42 +1292,47 @@
def create_boolean_collection(options)
options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic])
options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic])
options[:value_as_class] = true unless options.key?(:value_as_class)
- { options.delete(:true) => true, options.delete(:false) => false }
+ [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
end
# Used by association inputs (select, radio) to generate the name that should
# be used for the input
#
# belongs_to :author; f.input :author; will generate 'author_id'
+ # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
# has_many :authors; f.input :authors; will generate 'author_ids'
# has_and_belongs_to_many will act like has_many
#
def generate_association_input_name(method)
if reflection = find_reflection(method)
- method = "#{method.to_s.singularize}_id"
- method = method.pluralize if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
+ "#{method.to_s.singularize}_ids"
+ else
+ reflection.options[:foreign_key] || "#{method}_id"
+ end
+ else
+ method
end
- method
end
# If an association method is passed in (f.input :author) try to find the
# reflection object.
#
def find_reflection(method)
- object.class.reflect_on_association(method) if object.class.respond_to?(:reflect_on_association)
+ @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
end
# Generates default_string_options by retrieving column information from
# the database.
#
- def default_string_options(method) #:nodoc:
+ def default_string_options(method, type) #:nodoc:
column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
- if column.nil? || column.limit.nil?
+ if type == :numeric || column.nil? || column.limit.nil?
{ :size => @@default_text_field_size }
else
{ :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
end
end
@@ -1222,12 +1348,12 @@
elsif defined?(@auto_index)
index = "_#{@auto_index}"
else
index = ""
end
- sanitized_method_name = method_name.to_s.sub(/\?$/,"")
-
+ sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
+
"#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
end
# Gets the nested_child_index value from the parent builder. In Rails 2.3
# it always returns a fixnum. In next versions it returns a hash with each
@@ -1247,11 +1373,66 @@
def sanitized_object_name
@sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
end
- end
+ def humanized_attribute_name(method)
+ if @object && @object.class.respond_to?(:human_attribute_name)
+ @object.class.human_attribute_name(method.to_s)
+ else
+ method.to_s.send(@@label_str_method)
+ end
+ end
+ # Internal generic method for looking up localized values within Formtastic
+ # using I18n, if no explicit value is set and I18n-lookups are enabled.
+ #
+ # Enabled/Disable this by setting:
+ #
+ # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
+ #
+ # Lookup priority:
+ #
+ # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
+ # 'formtastic.{{type}}.{{model}}.{{attribute}}'
+ # 'formtastic.{{type}}.{{attribute}}'
+ #
+ # Example:
+ #
+ # 'formtastic.labels.post.edit.title'
+ # 'formtastic.labels.post.title'
+ # 'formtastic.labels.title'
+ #
+ # NOTE: Generic, but only used for form input labels/hints.
+ #
+ def localized_attribute_string(attr_name, attr_value, i18n_key)
+ if attr_value.is_a?(String)
+ attr_value
+ else
+ use_i18n = attr_value.nil? ? @@i18n_lookups_by_default : attr_value
+ if use_i18n
+ model_name = @object.class.name.underscore
+ action_name = template.params[:action].to_s rescue ''
+ attribute_name = attr_name.to_s
+
+ defaults = I18N_SCOPES.collect do |i18n_scope|
+ i18n_path = i18n_scope.dup
+ i18n_path.gsub!('{{action}}', action_name)
+ i18n_path.gsub!('{{model}}', model_name)
+ i18n_path.gsub!('{{attribute}}', attribute_name)
+ i18n_path.gsub!('..', '.')
+ i18n_path.to_sym
+ end
+ defaults << ''
+
+ i18n_value = ::I18n.t(defaults.shift, :default => defaults,
+ :scope => "formtastic.#{i18n_key.to_s.pluralize}")
+ i18n_value.blank? ? nil : i18n_value
+ end
+ end
+ end
+
+ end
# Wrappers around form_for (etc) with :builder => SemanticFormBuilder.
#
# * semantic_form_for(@post)
# * semantic_fields_for(@post)