lib/formtastic.rb in ShadowBelmolve-formtastic-0.2.1 vs lib/formtastic.rb in ShadowBelmolve-formtastic-0.9.7
- old
+ new
@@ -1,45 +1,33 @@
-# Override the default ActiveRecordHelper behaviour of wrapping the input.
-# This gets taken care of semantically by adding an error class to the LI tag
-# containing the input.
-ActionView::Base.field_error_proc = proc do |html_tag, instance_tag|
- html_tag
-end
+# coding: utf-8
+require File.join(File.dirname(__FILE__), *%w[formtastic i18n])
module Formtastic #:nodoc:
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
@@default_text_field_size = 50
@@all_fields_required_by_default = true
- @@required_string = proc { %{<abbr title="#{I18n.t 'formtastic.required', :default => 'required'}">*</abbr>} }
+ @@include_blank_for_select_by_default = true
+ @@required_string = proc { %{<abbr title="#{::Formtastic::I18n.t(:required)}">*</abbr>} }
@@optional_string = ''
@@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
+ @@default_commit_button_accesskey = nil
- 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, :priority_countries, :i18n_lookups_by_default
+ cattr_accessor :default_text_field_size, :all_fields_required_by_default, :include_blank_for_select_by_default,
+ :required_string, :optional_string, :inline_errors, :label_str_method, :collection_label_methods,
+ :inline_order, :file_methods, :priority_countries, :i18n_lookups_by_default, :default_commit_button_accesskey
- I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}',
- '{{model}}.{{attribute}}',
- '{{attribute}}']
+ RESERVED_COLUMNS = [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
- # Keeps simple mappings in a hash
- INPUT_MAPPINGS = {
- :string => :text_field,
- :password => :password_field,
- :numeric => :text_field,
- :text => :text_area,
- :file => :file_field
- }
- STRING_MAPPINGS = [ :string, :password, :numeric ]
+ INLINE_ERROR_TYPES = [:sentence, :list, :first]
attr_accessor :template
# Returns a suitable form input for the given +method+, using the database column information
# and other factors (like the method name) to figure out what you probably want.
@@ -76,38 +64,38 @@
#
# Example:
#
# <% semantic_form_for @employee do |form| %>
# <% form.inputs do -%>
- # <%= form.input :name, :label => "Full Name"%>
+ # <%= form.input :secret, :value => "Hello" %>
+ # <%= form.input :name, :label => "Full Name" %>
# <%= form.input :manager_id, :as => :radio %>
# <%= form.input :hired_at, :as => :date, :label => "Date Hired" %>
# <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %>
# <% end %>
# <% end %>
#
def input(method, options = {})
options[:required] = method_required?(method) unless options.key?(:required)
- options[:as] ||= default_input_type(method)
+ options[:as] ||= default_input_type(method, options)
html_class = [ options[:as], (options[:required] ? :required : :optional) ]
- html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors[method.to_sym]
+ html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
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
-
if options[:input_html] && options[:input_html][:id]
options[:label_html] ||= {}
options[:label_html][:for] ||= options[:input_html][:id]
end
- list_item_content = @@inline_order.map do |type|
+ input_parts = @@inline_order.dup
+ input_parts.delete(:errors) if options[:as] == :hidden
+
+ list_item_content = input_parts.map do |type|
send(:"inline_#{type}_for", method, options)
end.compact.join("\n")
return template.content_tag(:li, list_item_content, wrapper_html)
end
@@ -148,15 +136,20 @@
#
# <% semantic_form_for @post do |form| %>
# <%= form.inputs %>
# <% end %>
#
+ # With a few arguments:
+ # <% semantic_form_for @post do |form| %>
+ # <%= form.inputs "Post details", :title, :body %>
+ # <% end %>
+ #
# === Options
#
- # All options (with the exception of :name) are passed down to the fieldset as HTML
- # attributes (id, class, style, etc). If provided, the :name option is passed into a
- # legend tag inside the fieldset (otherwise a legend is not generated).
+ # All options (with the exception of :name/:title) are passed down to the fieldset as HTML
+ # attributes (id, class, style, etc). If provided, the :name/:title option is passed into a
+ # legend tag inside the fieldset.
#
# # With a block:
# <% semantic_form_for @post do |form| %>
# <% form.inputs :name => "Create a new post", :style => "border:1px;" do %>
# ...
@@ -166,10 +159,15 @@
# # With a list (the options must come after the field list):
# <% semantic_form_for @post do |form| %>
# <%= form.inputs :title, :body, :name => "Create a new post", :style => "border:1px;" %>
# <% end %>
#
+ # # ...or the equivalent:
+ # <% semantic_form_for @post do |form| %>
+ # <%= form.inputs "Create a new post", :title, :body, :style => "border:1px;" %>
+ # <% end %>
+ #
# === It's basically a fieldset!
#
# Instead of hard-coding fieldsets & legends into your form to logically group related fields,
# use inputs:
#
@@ -180,10 +178,13 @@
# <% end %>
# <% f.inputs :name => "Advanced", :id => "advanced" do %>
# <%= f.input :created_at %>
# <%= f.input :user_id, :label => "Author" %>
# <% end %>
+ # <% f.inputs "Extra" do %>
+ # <%= f.input :update_at %>
+ # <% end %>
# <% end %>
#
# # Output:
# <form ...>
# <fieldset class="inputs">
@@ -197,10 +198,16 @@
# <ol>
# <li class="datetime">...</li>
# <li class="select">...</li>
# </ol>
# </fieldset>
+ # <fieldset class="inputs">
+ # <legend><span>Extra</span></legend>
+ # <ol>
+ # <li class="datetime">...</li>
+ # </ol>
+ # </fieldset>
# </form>
#
# === Nested attributes
#
# As in Rails, you can use semantic_fields_for to nest attributes:
@@ -238,27 +245,31 @@
# inputs. If you have two separate models, but, semantically, on the page
# they are part of the same fieldset, you should use semantic_fields_for
# instead (just as you would do with Rails' form builder).
#
def inputs(*args, &block)
+ title = field_set_title_from_args(*args)
html_options = args.extract_options!
html_options[:class] ||= "inputs"
-
- if html_options[:for]
- inputs_for_nested_attributes(args, html_options, &block)
+ html_options[:name] = title
+
+ if html_options[:for] # Nested form
+ inputs_for_nested_attributes(*(args << html_options), &block)
elsif block_given?
- field_set_and_list_wrapping(html_options, &block)
+ field_set_and_list_wrapping(*(args << 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 lock_version]
+ args = self.association_columns(:belongs_to)
+ args += self.content_columns
+ args -= RESERVED_COLUMNS
args.compact!
end
- contents = args.map { |method| input(method.to_sym) }
-
- field_set_and_list_wrapping(html_options, contents)
+ legend = args.shift if args.first.is_a?(::String)
+ contents = args.collect { |method| input(method.to_sym) }
+ args.unshift(legend) if legend.present?
+
+ field_set_and_list_wrapping(*((args << html_options) << contents))
end
end
alias :input_field_set :inputs
# Creates a fieldset and ol tag wrapping for form buttons / actions as list items.
@@ -285,22 +296,39 @@
#
# <%= form.commit_button %> => <input name="commit" type="submit" value="Save Post" />
#
# The value of the button text can be overridden:
#
- # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
+ # <%= form.commit_button :label => "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
#
# 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" />
-
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
+ # <%= form.commit_button :class => "pretty" %> => <input name="commit" type="submit" value="Save Post" class="pretty {create|update|submit}" />
+ #
def commit_button(*args)
- value = args.first.is_a?(String) ? args.shift : save_or_create_button_text
- options = args.shift || {}
+ options = args.extract_options!
+ text = options.delete(:label) || args.shift
+
+ if @object
+ key = @object.new_record? ? :create : :update
+ object_name = @object.class.human_name
+ else
+ key = :submit
+ object_name = @object_name.to_s.send(@@label_str_method)
+ end
+
+ text = (self.localized_string(key, text, :action, :model => object_name) ||
+ ::Formtastic::I18n.t(key, :model => object_name)) unless text.is_a?(::String)
+
button_html = options.delete(:button_html) || {}
- template.content_tag(:li, self.submit(value, button_html), :class => "commit")
+ button_html.merge!(:class => [button_html[:class], key].compact.join(' '))
+ element_class = ['commit', options.delete(:class)].compact.join(' ') # TODO: Add class reflecting on form action.
+ accesskey = (options.delete(:accesskey) || @@default_commit_button_accesskey) unless button_html.has_key?(:accesskey)
+ button_html = button_html.merge(:accesskey => accesskey) if accesskey
+ template.content_tag(:li, self.submit(text, button_html), :class => element_class)
end
# A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
# for nesting forms:
#
@@ -320,85 +348,23 @@
# </fieldset>
# </form>
#
def semantic_fields_for(record_or_name_or_array, *args, &block)
opts = args.extract_options!
- opts.merge!(:builder => Formtastic::SemanticFormBuilder)
+ opts[:builder] ||= Formtastic::SemanticFormHelper.builder
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.
#
-
- # Add a link to remove the associated partial
+ # Add a link to remove the associated partial
# # Example:
# <% semantic_form_for @post do |post| %>
# <%= post.input :title %>
# <% post.inputs :name => 'Authors', :id => 'authors' do %>
# <%= post.add_associated_link "+ Author", :authors, :partial => 'authors/add_author' %>
@@ -410,11 +376,11 @@
# <div class="author">
# <%= f.input :name %>
# <%= f.remove_link "Remove" %>
# </div>
#
- # # Output:
+ # # Output:
# <form ...>
# <li class="string"><input type='text' name='post[author][name]' id='post_author_name' /></li>
# <fieldset class="inputs" id="authors"><legend><span>Authors</span></legend><ol>
# <a href="#" onclick="if (typeof formtastic_next_author_id == 'undefined') ....return false;">+ Author</a>
# <div class="author">
@@ -423,23 +389,23 @@
# <input id="post_authors__delete" name="post[authors][_delete]" type="hidden" />
# <a href="#" onclick="$(this).parents('.author').hide(); $(this).prev(':input').val('1');; return false;">Remove</a>
# </div>
# </ol></fieldset>
# </form>
- #
- # * Opts
+ #
+ # Opts:
+ #
# * :selector, id of element that will disappear(.hide()), if no id as give, will use css class
# association.class.name last word in downcase #=> '.author'
# f.remove_link "Remove", :selector => '#my_own_element_id'
#
# * :function, a funcion to execute before hide element and set _delete to 1
# f.remove_link "Remove", :function => "alert('Removing Author')"
#
#
# NOTE: remove_link must be put in the partial, not in main template
#
-
def remove_link(name, *args)
options = args.extract_options!
css_selector = options.delete(:selector) || ".#{@object.class.name.split("::").last.underscore}"
function = options.delete(:function) || ""
@@ -484,11 +450,11 @@
partial = opts.delete(:partial) || associated_name
container = opts.delete(:expression) || "'#{opts.delete(:container) || '#'+associated_name.pluralize}'"
before_function = opts.delete(:before_function) || ''
after_function = opts.delete(:after_function) || ''
- form = self.render_associated_form(object, :partial => partial)
+ form = render_associated_form(object, :partial => partial)
form.gsub!(/attributes_(\d+)/, '__idx__')
form.gsub!(/\[(\d+)\]/, '__idxx__')
function = "#{before_function};if (typeof #{variable} == 'undefined') #{variable} = #{$1 || 0};
$(#{container}).append(#{form.to_json}.replace(/__idx__/g, 'attributes_' +
@@ -496,63 +462,73 @@
template.link_to_function(name, function, opts)
end
# Render associated form
- # # Example:
+ #
+ # Example:
+ #
# <% semantic_form_for @post do |post| %>
# <%= post.input :title %>
# <% post.inputs :name => 'Authors', :id => 'authors' do %>
# <%= post.add_associated_link "+ Author", :authors, :partial => 'authors/add_author' %>
# <%= post.render_associated_form :authors, :partial => 'authors/add_author' %>
# <% end %>
# <% end %>
#
- # # app/views/authors/_add_author.html.erb
+ # Partial: app/views/authors/_add_author.html.erb
+ #
# <% f.input :name %>
#
- # # Output:
+ # Output:
+ #
# <form ...>
# <li class="string"><input type='text' name='post[author][name]' id='post_author_name' /></li>
# <fieldset class="inputs" id="authors"><legend><span>Authors</span></legend><ol>
# <a href="#" onclick="if (typeof formtastic_next_author_id == 'undefined') ....return false;">+ Author</a>
# <li class="string required" ...><label ...></label><input id="post_authors_name" maxlength="255"
# name="post[authors][name]" size="50" type="text" value="Renan T. Fernandes" /></li>
# </ol></fieldset>
# </form>
#
- # Opts
- # * :partial, render a partial, if not partial is given, try to find a partial with association
- # name in the controller folder.
+ # Opts:
+ # * :partial => Render a given partial, if no one is given, try to find a partial
+ # with association name in the controller folder.
+ #
# post.render_associated_form :authors, :partial => 'author'
# ^ try to render app/views/posts/_author.html.erb
+ #
# post.render_associated_form :authors, :partial => 'authors/author'
# ^ try to render app/views/authors/_author.html.erb
- # NOTE: Partial need to use 'f' as formtastic reference
- # Example:
+ #
+ # NOTE: Partial need to use 'f' as formtastic reference. Example:
+ #
# <%= f.input :name %> #=> render author name association
#
- # * :new make X empty partials if is a new record
- # Examples:
+ # * :new => make N empty partials if it is a new record. Example:
+ #
# post.render_associated_form :authors, :new => 2
# ^ make 2 empty authors partials
#
- # * :edit show only X partial if is editing a record
+ # * :edit => show only X partial if is editing a record. Example:
+ #
# post.render_associated_form :authors, :edit => 3
# ^ if record have 1 author, make 2 new empty partials
# NOTE: :edit not conflicts with :new; :new is for new records only
#
- # * :new_in_edit show X new partial if is editing a record
+ # * :new_in_edit => show X new partial if is editing a record. Example:
+ #
# post.render_associated_form :authors, :new_in_edit => 2
# ^ make more 2 new partial for record
# NOTE: this option is ignored if :edit is seted
- # Example:
- # post.render_associated_form :authors, :edit => 2, :new_in_edit => 100
- # ^ if record have 1 author, make more one partial
#
+ # Example:
#
- def render_associated_form(associated, opts = {})
+ # post.render_associated_form :authors, :edit => 2, :new_in_edit => 100
+ # ^ if record have 1 author, make more one partial
+ #
+ def render_associated_form(associated, opts = {})
associated = @object.send(associated.to_s) if associated.is_a? Symbol
associated = associated.is_a?(Array) ? associated : [associated] # preserve association proxy if this is one
opts.symbolize_keys!
(opts[:new] - associated.select(&:new_record?).length).times { associated.build } if opts[:new] and @object.new_record? == true
@@ -560,11 +536,10 @@
(opts[:edit] - associated.count).times { associated.build }
elsif opts[:new_in_edit] and @object.new_record? == false
opts[:new_in_edit].times { associated.build }
end
-
unless associated.empty?
name = extract_option_or_class_name(opts, :name, associated.first)
partial = opts[:partial] || name
local_assign_name = partial.split('/').last.split('.').first
@@ -575,864 +550,1218 @@
end
output.join
end
end
- 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
- # Prepare options to be sent to label
+ # 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:
#
- 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.
+ # * :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.
+ # * :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.
#
- # It should raise an error if a block with arity zero is given.
+ # == Examples
#
- def inputs_for_nested_attributes(args, options, &block)
- args << options.merge!(:parent => { :builder => self, :for => options[:for] })
-
- fields_for_block = if block_given?
- raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
- 'but the block does not accept any argument.' if block.arity <= 0
-
- proc { |f| f.inputs(*args){ block.call(f) } }
+ # 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
- proc { |f| f.inputs(*args) }
+ text = options_or_text
+ options ||= {}
end
+ text = localized_string(method, text, :label) || humanized_attribute_name(method)
+ text += required_or_optional_string(options.delete(:required))
- fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
- semantic_fields_for(*fields_for_args, &fields_for_block)
- end
+ # special case for boolean (checkbox) labels, which have a nested input
+ text = (options.delete(:label_prefix_for_nested_input) || "") + text
- # Remove any Formtastic-specific options before passing the down options.
- #
- def set_options(options)
- options.except(:value_method, :label_method, :collection, :required, :label,
- :as, :hint, :input_html, :label_html, :value_as_class)
+ input_name = options.delete(:input_name) || method
+ super(input_name, text, options)
end
- # Create a default button text. If the form is working with a object, it
- # defaults to "Create model" or "Save model" depending if we are working
- # with a new_record or not.
+ # Generates error messages for the given method. Errors can be shown as list,
+ # as sentence or just the first error can be displayed. If :none is set, no error is shown.
#
- # When not working with models, it defaults to "Submit object".
+ # This method is also aliased as errors_on, so you can call on your custom
+ # inputs as well:
#
- def save_or_create_button_text(prefix='Submit') #:nodoc:
- if @object
- prefix = @object.new_record? ? 'Create' : 'Save'
- object_name = @object.class.human_name
+ # semantic_form_for :post do |f|
+ # f.text_field(:body)
+ # f.errors_on(:body)
+ # end
+ #
+ def inline_errors_for(method, options = nil) #:nodoc:
+ if render_inline_errors?
+ errors = @object.errors[method.to_sym]
+ send(:"error_#{@@inline_errors}", [*errors]) if errors.present?
else
- object_name = @object_name.to_s.send(@@label_str_method)
+ nil
end
-
- I18n.t(prefix.downcase, :default => prefix, :scope => [:formtastic]) << ' ' << object_name
end
+ alias :errors_on :inline_errors_for
- # Determins if the attribute (eg :title) should be considered required or not.
- #
- # * if the :required option was provided in the options hash, the true/false value will be
- # returned immediately, allowing the view to override any guesswork that follows:
- #
- # * if the :required option isn't provided in the options hash, and the ValidationReflection
- # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
- # if the validates_presence_of macro has been used in the class for this attribute, or false
- # 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) #:nodoc:
- if @object && @object.class.respond_to?(:reflect_on_all_validations)
- attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
+ protected
- @object.class.reflect_on_all_validations.any? do |validation|
- validation.macro == :validates_presence_of && validation.name == attribute_sym
+ def render_inline_errors?
+ @object && @object.respond_to?(:errors) && INLINE_ERROR_TYPES.include?(@@inline_errors)
+ end
+
+ # Collects content columns (non-relation columns) for the current form object class.
+ #
+ def content_columns #:nodoc:
+ self.model_name.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue []
+ end
+
+ # Collects association columns (relation columns) for the current form object class.
+ #
+ def association_columns(*by_associations) #:nodoc:
+ if @object.present?
+ @object.class.reflections.collect do |name, _|
+ if by_associations.present?
+ name if by_associations.include?(_.macro)
+ else
+ name
+ end
+ end.compact
+ else
+ []
end
- else
- @@all_fields_required_by_default
end
- end
- # 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.
- #
- def input_simple(type, method, options)
- html_options = options.delete(:input_html) || {}
- html_options = default_string_options(method, type).merge(html_options) if STRING_MAPPINGS.include?(type)
+ # Prepare options to be sent to label
+ #
+ def options_for_label(options) #:nodoc:
+ options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
+ end
- self.label(method, options_for_label(options)) +
- self.send(INPUT_MAPPINGS[type], method, html_options)
- 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.
+ #
+ # It should raise an error if a block with arity zero is given.
+ #
+ def inputs_for_nested_attributes(*args, &block) #:nodoc:
+ options = args.extract_options!
+ args << options.merge!(:parent => { :builder => self, :for => options[:for] })
- # 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
+ fields_for_block = if block_given?
+ raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
+ 'but the block does not accept any argument.' if block.arity <= 0
+ proc { |f| f.inputs(*args){ block.call(f) } }
+ else
+ proc { |f| f.inputs(*args) }
+ 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
- #
- # Example (belongs_to):
- #
- # 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 (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
- # 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, :label_method => :full_name
- # f.input :author, :label_method => :display_name
- # f.input :author, :label_method => :to_s
- # f.input :author, :label_method => :label
- #
- # You can also customize the value inside each option tag, by passing in the :value_method option.
- # Usage is the same as the :label_method option
- #
- # Examples:
- #
- # f.input :author, :value_method => :full_name
- # f.input :author, :value_method => :display_name
- # f.input :author, :value_method => :to_s
- # f.input :author, :value_method => :value
- #
- # You can pass html_options to the select tag using :input_html => {}
- #
- # 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) || {}
+ fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
+ semantic_fields_for(*fields_for_args, &fields_for_block)
+ end
- unless options.key?(:include_blank) || options.key?(:prompt)
- options[:include_blank] = true
+ # Remove any Formtastic-specific options before passing the down options.
+ #
+ def strip_formtastic_options(options) #:nodoc:
+ options.except(:value_method, :label_method, :collection, :required, :label,
+ :as, :hint, :input_html, :label_html, :value_as_class)
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
+ # Determins if the attribute (eg :title) should be considered required or not.
+ #
+ # * if the :required option was provided in the options hash, the true/false value will be
+ # returned immediately, allowing the view to override any guesswork that follows:
+ #
+ # * if the :required option isn't provided in the options hash, and the ValidationReflection
+ # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
+ # if the validates_presence_of macro has been used in the class for this attribute, or false
+ # 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) #:nodoc:
+ if @object && @object.class.respond_to?(:reflect_on_validations_for)
+ attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
- input_name = generate_association_input_name(method)
- 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
+ @object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
+ validation.macro == :validates_presence_of &&
+ validation.name == attribute_sym &&
+ (validation.options.present? ? options_require_validation?(validation.options) : true)
+ end
+ else
+ @@all_fields_required_by_default
+ end
+ end
- # 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) || {}
+ # Determines whether the given options evaluate to true
+ def options_require_validation?(options) #nodoc
+ if_condition = !options[:if].nil?
+ condition = if_condition ? options[:if] : options[:unless]
- self.label(method, options_for_label(options)) +
- self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options)
- end
+ condition = if condition.respond_to?(:call)
+ condition.call(@object)
+ elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
+ @object.send(condition)
+ else
+ condition
+ 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.
- #
- # Example:
- #
- # f.input :author, :as => :radio
- #
- # Output:
- #
- # <fieldset>
- # <legend><span>Author</span></legend>
- # <ol>
- # <li>
- # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
- # </li>
- # <li>
- # <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 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.
- #
- # Examples:
- #
- # f.input :author, :as => :radio, :label_method => :full_name
- # f.input :author, :as => :radio, :label_method => :display_name
- # f.input :author, :as => :radio, :label_method => :to_s
- # f.input :author, :as => :radio, :label_method => :label
- #
- # Finally, you can set :value_as_class => true if you want that LI wrappers
- # contains a class with the wrapped radio input value.
- #
- def radio_input(method, options)
- collection = find_collection_for_column(method, options)
- html_options = set_options(options).merge(options.delete(:input_html) || {})
+ if_condition ? !!condition : !condition
+ end
- input_name = generate_association_input_name(method)
- value_as_class = options.delete(:value_as_class)
+ def basic_input_helper(form_helper_method, type, method, options) #:nodoc:
+ html_options = options.delete(:input_html) || {}
+ html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password].include?(type)
- list_item_content = collection.map do |c|
- label = c.is_a?(Array) ? c.first : c
- value = c.is_a?(Array) ? c.last : c
+ self.label(method, options_for_label(options)) <<
+ self.send(form_helper_method, method, html_options)
+ end
- li_content = template.content_tag(:label,
- "#{self.radio_button(input_name, value, html_options)} #{label}",
- :for => generate_html_id(input_name, value.to_s.downcase)
- )
+ # Outputs a label and standard Rails text field inside the wrapper.
+ def string_input(method, options)
+ basic_input_helper(:text_field, :string, method, options)
+ end
- li_options = value_as_class ? { :class => value.to_s.downcase } : {}
- template.content_tag(:li, li_content, li_options)
+ # Outputs a label and standard Rails password field inside the wrapper.
+ def password_input(method, options)
+ basic_input_helper(:password_field, :password, method, options)
end
- field_set_and_list_wrapping_for_method(method, options, list_item_content)
- end
- alias :boolean_radio_input :radio_input
+ # Outputs a label and standard Rails text field inside the wrapper.
+ def numeric_input(method, options)
+ basic_input_helper(:text_field, :numeric, method, options)
+ end
- # 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.
- def date_input(method, options)
- date_or_datetime_input(method, options.merge(:discard_hour => true))
- end
+ # Ouputs a label and standard Rails text area inside the wrapper.
+ def text_input(method, options)
+ basic_input_helper(:text_area, :text, method, options)
+ end
+ # Outputs a label and a standard Rails file field inside the wrapper.
+ def file_input(method, options)
+ basic_input_helper(:file_field, :file, method, options)
+ end
- # 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, hour, min, sec). 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.
- def datetime_input(method, options)
- date_or_datetime_input(method, 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)
+ options ||= {}
+ if options[:input_html].present?
+ options[:value] = options[:input_html][:value] if options[:input_html][:value].present?
+ end
+ self.hidden_field(method, strip_formtastic_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
+ #
+ # Example (belongs_to):
+ #
+ # 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 (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"]
+ #
+ # The :label_method option allows you to customize the text label inside each option tag two ways:
+ #
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
+ #
+ # Examples:
+ #
+ # f.input :author, :label_method => :full_name
+ # f.input :author, :label_method => :login
+ # f.input :author, :label_method => :full_name_with_post_count
+ # f.input :author, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
+ #
+ # The :value_method option provides the same customization of the value attribute of each option tag.
+ #
+ # Examples:
+ #
+ # f.input :author, :value_method => :full_name
+ # f.input :author, :value_method => :login
+ # f.input :author, :value_method => Proc.new { |a| "author_#{a.login}" }
+ #
+ # You can pre-select a specific option value by passing in the :selected option.
+ #
+ # Examples:
+ #
+ # f.input :author, :selected => current_user.id
+ # f.input :author, :value_method => :login, :selected => current_user.login
+ # f.input :authors, :value_method => :login, :selected => Author.most_popular.collect(&:id)
+ # f.input :authors, :value_method => :login, :selected => nil # override any defaults: select none
+ #
+ # You can pass html_options to the select tag using :input_html => {}
+ #
+ # 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.
+ #
+ #
+ # You can group the options in optgroup elements by passing the :group_by option
+ # (Note: only tested for belongs_to relations)
+ #
+ # Examples:
+ #
+ # f.input :author, :group_by => :continent
+ #
+ # All the other options should work as expected. If you want to call a custom method on the
+ # group item. You can include the option:group_label_method
+ # Examples:
+ #
+ # f.input :author, :group_by => :continents, :group_label_method => :something_different
+ #
+ def select_input(method, options)
+ html_options = options.delete(:input_html) || {}
+ options = set_include_blank(options)
+ html_options[:multiple] = html_options[:multiple] || options.delete(:multiple)
+ html_options.delete(:multiple) if html_options[:multiple].nil?
- # 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 time (hour, minute, second). Each li contains a label
- # (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
- #
- # Some of Rails' options for select_time are supported, but not everything yet.
- def time_input(method, options)
- date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
- end
+ reflection = self.reflection_for(method)
+ if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
+ options[:include_blank] = false
+ html_options[:multiple] = true if html_options[:multiple].nil?
+ html_options[:size] ||= 5
+ end
+ options[:selected] = options[:selected].first if options[:selected].present? && html_options[:multiple] == false
+ input_name = generate_association_input_name(method)
+ select_html = if options[:group_by]
+ # The grouped_options_select is a bit counter intuitive and not optimised (mostly due to ActiveRecord).
+ # The formtastic user however shouldn't notice this too much.
+ raw_collection = find_raw_collection_for_column(method, options.reverse_merge(:find_options => { :include => options[:group_by] }))
+ label, value = detect_label_and_value_method!(raw_collection)
+ group_collection = raw_collection.map { |option| option.send(options[:group_by]) }.uniq
+ group_label_method = options[:group_label_method] || detect_label_method(group_collection)
+ group_collection = group_collection.sort_by { |group_item| group_item.send(group_label_method) }
+ group_association = options[:group_association] || detect_group_association(method, options[:group_by])
- # <fieldset>
- # <legend>Created At</legend>
- # <ol>
- # <li>
- # <label for="user_created_at_1i">Year</label>
- # <select id="user_created_at_1i" name="user[created_at(1i)]">
- # <option value="2003">2003</option>
- # ...
- # <option value="2013">2013</option>
- # </select>
- # </li>
- # <li>
- # <label for="user_created_at_2i">Month</label>
- # <select id="user_created_at_2i" name="user[created_at(2i)]">
- # <option value="1">January</option>
- # ...
- # <option value="12">December</option>
- # </select>
- # </li>
- # <li>
- # <label for="user_created_at_3i">Day</label>
- # <select id="user_created_at_3i" name="user[created_at(3i)]">
- # <option value="1">1</option>
- # ...
- # <option value="31">31</option>
- # </select>
- # </li>
- # </ol>
- # </fieldset>
- #
- # This is an absolute abomination, but so is the official Rails select_date().
- #
- def date_or_datetime_input(method, options)
- position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
- inputs = options.delete(:order) || I18n.translate(:'date.order') || [:year, :month, :day]
+ # Here comes the monster with 8 arguments
+ self.grouped_collection_select(input_name, group_collection,
+ group_association, group_label_method,
+ value, label,
+ strip_formtastic_options(options), html_options)
+ else
+ collection = find_collection_for_column(method, options)
- time_inputs = [:hour, :minute]
- time_inputs << [:second] if options[:include_seconds]
+ self.select(input_name, collection, strip_formtastic_options(options), html_options)
+ end
- list_items_capture = ""
- hidden_fields_capture = ""
+ self.label(method, options_for_label(options).merge(:input_name => input_name)) << select_html
+ end
+ alias :boolean_select_input :select_input
- # 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) || {}
+ # 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/
+ #
+ # You can pre-select a specific option value by passing in the :selected option.
+ # Note: Right now only works if the form object attribute value is not set (nil),
+ # because of how the core helper is implemented.
+ #
+ # Examples:
+ #
+ # f.input :my_favorite_time_zone, :as => :time_zone, :selected => 'Singapore'
+ #
+ def time_zone_input(method, options)
+ html_options = options.delete(:input_html) || {}
+ selected_value = options.delete(:selected)
- (inputs + time_inputs).each do |input|
- html_id = generate_html_id(method, "#{position[input]}i")
- field_name = "#{method}(#{position[input]}i)"
- if options["discard_#{input}".intern]
- break if time_inputs.include?(input)
-
- hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime
- 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])
+ self.label(method, options_for_label(options)) <<
+ self.time_zone_select(method, options.delete(:priority_zones),
+ strip_formtastic_options(options).merge(:default => selected_value), html_options)
+ end
- 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))
+ # 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.
+ #
+ # Example:
+ #
+ # f.input :author, :as => :radio
+ #
+ # Output:
+ #
+ # <fieldset>
+ # <legend><span>Author</span></legend>
+ # <ol>
+ # <li>
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
+ # </li>
+ # <li>
+ # <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 choices available in the radio button set by passing in a collection (an Array or
+ # Hash) through the :collection option. If not provided, the choices are found by reflecting on the association
+ # (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"]
+ #
+ # The :label_method option allows you to customize the label for each radio button two ways:
+ #
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
+ #
+ # Examples:
+ #
+ # f.input :author, :as => :radio, :label_method => :full_name
+ # f.input :author, :as => :radio, :label_method => :login
+ # f.input :author, :as => :radio, :label_method => :full_name_with_post_count
+ # f.input :author, :as => :radio, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
+ #
+ # The :value_method option provides the same customization of the value attribute of each option tag.
+ #
+ # Examples:
+ #
+ # f.input :author, :as => :radio, :value_method => :full_name
+ # f.input :author, :as => :radio, :value_method => :login
+ # f.input :author, :as => :radio, :value_method => Proc.new { |a| "author_#{a.login}" }
+ #
+ # You can force a particular radio button in the collection to be checked with the :selected option.
+ #
+ # Examples:
+ #
+ # f.input :subscribe_to_newsletter, :as => :radio, :selected => true
+ # f.input :subscribe_to_newsletter, :as => :radio, :collection => ["Yeah!", "Nope!"], :selected => "Nope!"
+ #
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each radio
+ # button / label combination to contain a class with the value of the radio button (useful for
+ # applying specific CSS or Javascript to a particular radio button).
+ #
+ def radio_input(method, options)
+ collection = find_collection_for_column(method, options)
+ html_options = strip_formtastic_options(options).merge(options.delete(:input_html) || {})
+
+ input_name = generate_association_input_name(method)
+ value_as_class = options.delete(:value_as_class)
+ input_ids = []
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
+ selected_value = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
+
+ list_item_content = collection.map do |c|
+ label = c.is_a?(Array) ? c.first : c
+ value = c.is_a?(Array) ? c.last : c
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
+ input_ids << input_id
+
+ html_options[:checked] = selected_value == value if selected_option_is_present
+
+ li_content = template.content_tag(:label,
+ "#{self.radio_button(input_name, value, html_options)} #{label}",
+ :for => input_id
)
+
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
+ template.content_tag(:li, li_content, li_options)
end
+
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
end
+ alias :boolean_radio_input :radio_input
- hidden_fields_capture + field_set_and_list_wrapping_for_method(method, options, list_items_capture)
- end
+ # 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.
+ #
+ # You can pre-select a specific option value by passing in the :selected option.
+ #
+ # Examples:
+ #
+ # f.input :created_at, :as => :date, :selected => 1.day.ago
+ # f.input :created_at, :as => :date, :selected => nil # override any defaults: select none
+ #
+ # Some of Rails' options for select_date are supported, but not everything yet.
+ #
+ def date_input(method, options)
+ options = set_include_blank(options)
+ date_or_datetime_input(method, options.merge(:discard_hour => true))
+ end
+ # 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, hour, min, sec). Each li
+ # contains a label (eg "Year") and a select box. See date_or_datetime_input for a more
+ # detailed output example.
+ #
+ # You can pre-select a specific option value by passing in the :selected option.
+ #
+ # Examples:
+ #
+ # f.input :created_at, :as => :datetime, :selected => 1.day.ago
+ # f.input :created_at, :as => :datetime, :selected => nil # override any defaults: select none
+ #
+ # Some of Rails' options for select_date are supported, but not everything yet.
+ #
+ def datetime_input(method, options)
+ options = set_include_blank(options)
+ date_or_datetime_input(method, 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 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) || {}
+ # 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 time (hour, minute, second). Each li contains a label
+ # (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
+ #
+ # You can pre-select a specific option value by passing in the :selected option.
+ #
+ # Examples:
+ #
+ # f.input :created_at, :as => :time, :selected => 1.hour.ago
+ # f.input :created_at, :as => :time, :selected => nil # override any defaults: select none
+ #
+ # Some of Rails' options for select_time are supported, but not everything yet.
+ #
+ def time_input(method, options)
+ options = set_include_blank(options)
+ date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
+ end
- 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)
+ # <fieldset>
+ # <legend>Created At</legend>
+ # <ol>
+ # <li>
+ # <label for="user_created_at_1i">Year</label>
+ # <select id="user_created_at_1i" name="user[created_at(1i)]">
+ # <option value="2003">2003</option>
+ # ...
+ # <option value="2013">2013</option>
+ # </select>
+ # </li>
+ # <li>
+ # <label for="user_created_at_2i">Month</label>
+ # <select id="user_created_at_2i" name="user[created_at(2i)]">
+ # <option value="1">January</option>
+ # ...
+ # <option value="12">December</option>
+ # </select>
+ # </li>
+ # <li>
+ # <label for="user_created_at_3i">Day</label>
+ # <select id="user_created_at_3i" name="user[created_at(3i)]">
+ # <option value="1">1</option>
+ # ...
+ # <option value="31">31</option>
+ # </select>
+ # </li>
+ # </ol>
+ # </fieldset>
+ #
+ # This is an absolute abomination, but so is the official Rails select_date().
+ #
+ def date_or_datetime_input(method, options)
+ position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
+ i18n_date_order = ::I18n.t(:order, :scope => [:date])
+ i18n_date_order = nil unless i18n_date_order.is_a?(Array)
+ inputs = options.delete(:order) || i18n_date_order || [:year, :month, :day]
- list_item_content = collection.map do |c|
- label = c.is_a?(Array) ? c.first : c
- value = c.is_a?(Array) ? c.last : c
+ time_inputs = [:hour, :minute]
+ time_inputs << [:second] if options[:include_seconds]
- 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]
- )
+ list_items_capture = ""
+ hidden_fields_capture = ""
- li_options = value_as_class ? { :class => value.to_s.downcase } : {}
- template.content_tag(:li, li_content, li_options)
+ default_time = ::Time.now
+
+ # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
+ datetime = options[:selected] || (@object ? @object.send(method) : default_time) || default_time
+
+ html_options = options.delete(:input_html) || {}
+ input_ids = []
+
+ (inputs + time_inputs).each do |input|
+ input_ids << input_id = generate_html_id(method, "#{position[input]}i")
+
+ field_name = "#{method}(#{position[input]}i)"
+ if options[:"discard_#{input}"]
+ break if time_inputs.include?(input)
+
+ hidden_value = datetime.respond_to?(input) ? datetime.send(input.to_sym) : datetime
+ hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => input_id)
+ else
+ opts = strip_formtastic_options(options).merge(:prefix => @object_name, :field_name => field_name, :default => datetime)
+ item_label_text = ::I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
+
+ list_items_capture << template.content_tag(:li,
+ template.content_tag(:label, item_label_text, :for => input_id) <<
+ template.send(:"select_#{input}", datetime, opts, html_options.merge(:id => input_id))
+ )
+ end
+ end
+
+ hidden_fields_capture << field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_items_capture)
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
+ # 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 class="label"><label>Authors</label></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]
+ #
+ # The :label_method option allows you to customize the label for each checkbox two ways:
+ #
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
+ #
+ # Examples:
+ #
+ # f.input :author, :as => :check_boxes, :label_method => :full_name
+ # f.input :author, :as => :check_boxes, :label_method => :login
+ # f.input :author, :as => :check_boxes, :label_method => :full_name_with_post_count
+ # f.input :author, :as => :check_boxes, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
+ #
+ # The :value_method option provides the same customization of the value attribute of each checkbox input tag.
+ #
+ # Examples:
+ #
+ # f.input :author, :as => :check_boxes, :value_method => :full_name
+ # f.input :author, :as => :check_boxes, :value_method => :login
+ # f.input :author, :as => :check_boxes, :value_method => Proc.new { |a| "author_#{a.login}" }
+ #
+ # You can pre-select/check a specific checkbox value by passing in the :selected option (alias :checked works as well).
+ #
+ # Examples:
+ #
+ # f.input :authors, :as => :check_boxes, :selected => @justin
+ # f.input :authors, :as => :check_boxes, :selected => Author.most_popular.collect(&:id)
+ # f.input :authors, :as => :check_boxes, :selected => nil # override any defaults: select none
+ #
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each checkbox / label
+ # combination to contain a class with the value of the radio button (useful for applying specific
+ # CSS or Javascript to a particular checkbox).
+ #
+ def check_boxes_input(method, options)
+ collection = find_collection_for_column(method, options)
+ html_options = options.delete(:input_html) || {}
- self.label(method, options_for_label(options)) +
- self.country_select(method, priority_countries, set_options(options), html_options)
- end
-
+ 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)
+ input_ids = []
- # 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.
- # :checked_value and :unchecked_value options are also available.
- #
- def boolean_input(method, options)
- html_options = options.delete(:input_html) || {}
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
+ selected_values = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
+ selected_values = [*selected_values].compact
+
+ list_item_content = collection.map do |c|
+ label = c.is_a?(Array) ? c.first : c
+ value = c.is_a?(Array) ? c.last : c
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
+ input_ids << input_id
- input = self.check_box(method, set_options(options).merge(html_options),
- options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
+ html_options[:checked] = selected_values.include?(value) if selected_option_is_present
+ html_options[:id] = input_id
- label = options.delete(:label) || humanized_attribute_name(method)
- self.label(method, input + label, options_for_label(options))
- end
+ li_content = template.content_tag(:label,
+ "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
+ :for => input_id
+ )
- # 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
- # implementation which maps most of the inputs. All others have specific
- # code and then a proper handler should be called (like radio_input) for
- # :radio types.
- #
- def inline_input_for(method, options)
- input_type = options.delete(:as)
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
+ template.content_tag(:li, li_content, li_options)
+ end
- if INPUT_MAPPINGS.key?(input_type)
- input_simple(input_type, method, options)
- else
- send("#{input_type}_input", method, options)
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
end
- end
- # Generates hints for the given method using the text supplied in :hint.
- #
- def inline_hints_for(method, options) #:nodoc:
- options[:hint] = localized_attribute_string(method, options[:hint], :hint)
- return if options[:hint].blank?
- template.content_tag(:p, options[:hint], :class => 'inline-hints')
- 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
- # Creates an error sentence by calling to_sentence on the errors array.
- #
- def error_sentence(errors) #:nodoc:
- template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
- end
+ self.label(method, options_for_label(options)) <<
+ self.country_select(method, priority_countries, strip_formtastic_options(options), html_options)
+ end
- # Creates an error li list.
- #
- def error_list(errors) #:nodoc:
- list_elements = []
- errors.each do |error|
- list_elements << template.content_tag(:li, error.untaint)
+ # 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.
+ # :checked_value and :unchecked_value options are also available.
+ #
+ # You can pre-select/check the boolean checkbox by passing in the :selected option (alias :checked works as well).
+ #
+ # Examples:
+ #
+ # f.input :allow_comments, :as => :boolean, :selected => true # override any default value: selected/checked
+ #
+ def boolean_input(method, options)
+ html_options = options.delete(:input_html) || {}
+ checked = options.key?(:checked) ? options[:checked] : options[:selected]
+ html_options[:checked] = checked == true if [:selected, :checked].any? { |k| options.key?(k) }
+
+ input = self.check_box(method, strip_formtastic_options(options).merge(html_options),
+ options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
+ options = options_for_label(options)
+
+ # the label() method will insert this nested input into the label at the last minute
+ options[:label_prefix_for_nested_input] = input
+
+ self.label(method, options)
end
- template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
- 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 = case required
- when true
- @@required_string
- when false
- @@optional_string
- else
- required
+ # Generates an input for the given method using the type supplied with :as.
+ def inline_input_for(method, options)
+ send(:"#{options.delete(:as)}_input", method, options)
end
- if string_or_proc.is_a?(Proc)
- string_or_proc.call
- else
- string_or_proc.to_s
+ # Generates hints for the given method using the text supplied in :hint.
+ #
+ def inline_hints_for(method, options) #:nodoc:
+ options[:hint] = localized_string(method, options[:hint], :hint)
+ return if options[:hint].blank?
+ template.content_tag(:p, options[:hint], :class => 'inline-hints')
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
- # in :name. So you can do:
- #
- # f.inputs :name => 'Task #%i', :for => :tasks
- #
- # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
- # 'Task #3' and so on.
- #
- def field_set_and_list_wrapping(html_options, contents='', &block) #:nodoc:
- legend = html_options.delete(:name).to_s
- legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
- legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
+ # Creates an error sentence by calling to_sentence on the errors array.
+ #
+ def error_sentence(errors) #:nodoc:
+ template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
+ end
- contents = template.capture(&block) if block_given?
+ # Creates an error li list.
+ #
+ def error_list(errors) #:nodoc:
+ list_elements = []
+ errors.each do |error|
+ list_elements << template.content_tag(:li, error.untaint)
+ end
+ template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
+ end
- # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
- contents = contents.join if contents.respond_to?(:join)
- fieldset = template.content_tag(:fieldset,
- legend + template.content_tag(:ol, contents),
- html_options.except(:builder, :parent)
- )
+ # Creates an error sentence containing only the first error
+ #
+ def error_first(errors) #:nodoc:
+ template.content_tag(:p, errors.first.untaint, :class => 'inline-errors')
+ end
- template.concat(fieldset) if block_given?
- fieldset
- 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 = case required
+ when true
+ @@required_string
+ when false
+ @@optional_string
+ else
+ required
+ end
- # 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)
+ if string_or_proc.is_a?(Proc)
+ string_or_proc.call
+ else
+ string_or_proc.to_s
+ end
+ end
- template.content_tag(:fieldset,
- %{<legend>#{self.label(method, options_for_label(options).merge!(:as_span => true))}</legend>} +
- template.content_tag(:ol, contents)
- )
- 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
+ # in :name. So you can do:
+ #
+ # f.inputs :name => 'Task #%i', :for => :tasks
+ #
+ # or the shorter equivalent:
+ #
+ # f.inputs 'Task #%i', :for => :tasks
+ #
+ # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
+ # 'Task #3' and so on.
+ #
+ # Note: Special case for the inline inputs (non-block):
+ # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
+ # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
+ # f.inputs :title, :body, :author # First argument is a column => (no legend)
+ #
+ def field_set_and_list_wrapping(*args, &block) #:nodoc:
+ contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
+ html_options = args.extract_options!
- # 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:
- column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
+ legend = html_options.delete(:name).to_s
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
+ legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
- 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 :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/
+ if block_given?
+ contents = if template.respond_to?(:is_haml?) && template.is_haml?
+ template.capture_haml(&block)
+ else
+ template.capture(&block)
+ end
+ end
- # otherwise assume the input name will be the same as the column type (eg string_input)
- return column.type
- else
- if @object
- return :select if find_reflection(method)
+ # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
+ contents = contents.join if contents.respond_to?(:join)
+ fieldset = template.content_tag(:fieldset,
+ legend << template.content_tag(:ol, contents),
+ html_options.except(:builder, :parent)
+ )
- file = @object.send(method) if @object.respond_to?(method)
- return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
- end
+ template.concat(fieldset) if block_given?
+ fieldset
+ end
- return :password if method.to_s =~ /password/
- return :string
+ def field_set_title_from_args(*args) #:nodoc:
+ options = args.extract_options!
+ options[:name] ||= options.delete(:title)
+ title = options[:name]
+
+ if title.blank?
+ valid_name_classes = [::String, ::Symbol]
+ valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first))
+ title = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
+ end
+ title = localized_string(title, title, :title) if title.is_a?(::Symbol)
+ title
end
- end
- # Used by select and radio inputs. The collection can be retrieved by
- # three ways:
- #
- # * Explicitly provided through :collection
- # * Retrivied through an association
- # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
- #
- # If the collection is not a hash or an array of strings, fixnums or arrays,
- # we use label_method and value_method to retreive an array with the
- # appropriate label and value.
- #
- def find_collection_for_column(column, options)
- reflection = find_reflection(column)
+ # 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) #:nodoc:
+ contents = contents.join if contents.respond_to?(:join)
- collection = if options[:collection]
- options.delete(:collection)
- elsif reflection || column.to_s =~ /_id$/
- parent_class = if reflection
- reflection.klass
+ template.content_tag(:fieldset,
+ template.content_tag(:legend,
+ self.label(method, options_for_label(options).merge(:for => options.delete(:label_for))), :class => 'label'
+ ) <<
+ template.content_tag(:ol, contents)
+ )
+ end
+
+ # 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, options = {}) #:nodoc:
+ if column = self.column_for(method)
+ # Special cases where the column type doesn't map to an input method.
+ case column.type
+ when :string
+ return :password if method.to_s =~ /password/
+ return :country if method.to_s =~ /country/
+ return :time_zone if method.to_s =~ /time_zone/
+ when :integer
+ return :select if method.to_s =~ /_id$/
+ return :numeric
+ when :float, :decimal
+ return :numeric
+ when :timestamp
+ return :datetime
+ end
+
+ # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
+ return :select if column.type == :string && options.key?(:collection)
+ # Try 3: Assume the input name will be the same as the column type (e.g. string_input).
+ return column.type
else
- ::ActiveSupport::Deprecation.warn("The _id way of doing things is deprecated. Please use the association method (#{column.to_s.sub(/_id$/,'')})", caller[3..-1])
- column.to_s.sub(/_id$/,'').camelize.constantize
+ if @object
+ return :select if self.reflection_for(method)
+
+ file = @object.send(method) if @object.respond_to?(method)
+ return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
+ end
+
+ return :select if options.key?(:collection)
+ return :password if method.to_s =~ /password/
+ return :string
end
+ end
- parent_class.find(:all)
- else
- create_boolean_collection(options)
+ # Used by select and radio inputs. The collection can be retrieved by
+ # three ways:
+ #
+ # * Explicitly provided through :collection
+ # * Retrivied through an association
+ # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
+ #
+ # If the collection is not a hash or an array of strings, fixnums or arrays,
+ # we use label_method and value_method to retreive an array with the
+ # appropriate label and value.
+ #
+ def find_collection_for_column(column, options) #:nodoc:
+ collection = find_raw_collection_for_column(column, options)
+
+ # Return if we have an Array of strings, fixnums or arrays
+ return collection if (collection.instance_of?(Array) || collection.instance_of?(Range)) &&
+ [Array, Fixnum, String, Symbol].include?(collection.first.class)
+
+ label, value = detect_label_and_value_method!(collection, options)
+ collection.map { |o| [send_or_call(label, o), send_or_call(value, o)] }
end
- collection = collection.to_a if collection.instance_of?(Hash)
+ # As #find_collection_for_column but returns the collection without mapping the label and value
+ #
+ def find_raw_collection_for_column(column, options) #:nodoc:
+ collection = if options[:collection]
+ options.delete(:collection)
+ elsif reflection = self.reflection_for(column)
+ reflection.klass.find(:all, options[:find_options] || {})
+ else
+ create_boolean_collection(options)
+ end
- # Return if we have an Array of strings, fixnums or arrays
- return collection if collection.instance_of?(Array) &&
- [Array, Fixnum, String, Symbol].include?(collection.first.class)
+ collection = collection.to_a if collection.is_a?(Hash)
+ collection
+ end
- label = options.delete(:label_method) || detect_label_method(collection)
- value = options.delete(:value_method) || :id
+ # Detects the label and value methods from a collection values set in
+ # @@collection_label_methods. It will use and delete
+ # the options :label_method and :value_methods when present
+ #
+ def detect_label_and_value_method!(collection_or_instance, options = {}) #:nodoc
+ label = options.delete(:label_method) || detect_label_method(collection_or_instance)
+ value = options.delete(:value_method) || :id
+ [label, value]
+ end
- collection.map { |o| [o.send(label), o.send(value)] }
- end
+ # Detected the label collection method when none is supplied using the
+ # values set in @@collection_label_methods.
+ #
+ def detect_label_method(collection) #:nodoc:
+ @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
+ end
- # Detected the label collection method when none is supplied using the
- # values set in @@collection_label_methods.
- #
- def detect_label_method(collection) #:nodoc:
- @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
- end
+ # Detects the method to call for fetching group members from the groups when grouping select options
+ #
+ def detect_group_association(method, group_by)
+ object_to_method_reflection = self.reflection_for(method)
+ method_class = object_to_method_reflection.klass
+
+ method_to_group_association = method_class.reflect_on_association(group_by)
+ group_class = method_to_group_association.klass
+
+ # This will return in the normal case
+ return method.to_s.pluralize.to_sym if group_class.reflect_on_association(method.to_s.pluralize)
+
+ # This is for belongs_to associations named differently than their class
+ # form.input :parent, :group_by => :customer
+ # eg.
+ # class Project
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
+ # belongs_to :customer
+ # end
+ # class Customer
+ # has_many :projects
+ # end
+ group_method = method_class.to_s.underscore.pluralize.to_sym
+ return group_method if group_class.reflect_on_association(group_method) # :projects
+
+ # This is for has_many associations named differently than their class
+ # eg.
+ # class Project
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
+ # belongs_to :customer
+ # end
+ # class Customer
+ # has_many :tasks, :class_name => 'Project', :foreign_key => 'customer_id'
+ # end
+ possible_associations = group_class.reflect_on_all_associations(:has_many).find_all{|assoc| assoc.klass == object_class}
+ return possible_associations.first.name.to_sym if possible_associations.count == 1
+
+ raise "Cannot infer group association for #{method} grouped by #{group_by}, there were #{possible_associations.empty? ? 'no' : possible_associations.size} possible associations. Please specify using :group_association"
+
+ end
- # Returns a hash to be used by radio and select inputs when a boolean field
- # is provided.
- #
- 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)
+ # Returns a hash to be used by radio and select inputs when a boolean field
+ # is provided.
+ #
+ def create_boolean_collection(options) #:nodoc:
+ options[:true] ||= ::Formtastic::I18n.t(:yes)
+ options[:false] ||= ::Formtastic::I18n.t(:no)
+ options[:value_as_class] = true unless options.key?(:value_as_class)
- [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
- end
+ [ [ 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)
- if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
- "#{method.to_s.singularize}_ids"
+ # 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) #:nodoc:
+ if reflection = self.reflection_for(method)
+ 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
- reflection.options[:foreign_key] || "#{method}_id"
- end
- else
- method
+ method
+ end.to_sym
end
- 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)
- end
+ # If an association method is passed in (f.input :author) try to find the
+ # reflection object.
+ #
+ def reflection_for(method) #:nodoc:
+ @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, type) #:nodoc:
- column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
+ # Get a column object for a specified attribute method - if possible.
+ #
+ def column_for(method) #:nodoc:
+ @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
+ end
- 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 }
+ # Generates default_string_options by retrieving column information from
+ # the database.
+ #
+ def default_string_options(method, type) #:nodoc:
+ column = self.column_for(method)
+
+ 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
- end
- # Generate the html id for the li tag.
- # It takes into account options[:index] and @auto_index to generate li
- # elements with appropriate index scope. It also sanitizes the object
- # and method names.
- #
- def generate_html_id(method_name, value='input')
- if options.has_key?(:index)
- index = "_#{options[:index]}"
- elsif defined?(@auto_index)
- index = "_#{@auto_index}"
- else
- index = ""
+ # Generate the html id for the li tag.
+ # It takes into account options[:index] and @auto_index to generate li
+ # elements with appropriate index scope. It also sanitizes the object
+ # and method names.
+ #
+ def generate_html_id(method_name, value='input') #:nodoc:
+ if options.has_key?(:index)
+ index = "_#{options[:index]}"
+ elsif defined?(@auto_index)
+ index = "_#{@auto_index}"
+ else
+ index = ""
+ end
+ sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
+
+ "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
end
- 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
- # association that the parent builds.
- #
- def parent_child_index(parent)
- duck = parent[:builder].instance_variable_get('@nested_child_index')
+ # 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
+ # association that the parent builds.
+ #
+ def parent_child_index(parent) #:nodoc:
+ duck = parent[:builder].instance_variable_get('@nested_child_index')
- if duck.is_a?(Hash)
- child = parent[:for]
- child = child.first if child.respond_to?(:first)
- duck[child].to_i + 1
- else
- duck.to_i + 1
+ if duck.is_a?(Hash)
+ child = parent[:for]
+ child = child.first if child.respond_to?(:first)
+ duck[child].to_i + 1
+ else
+ duck.to_i + 1
+ end
end
- end
- def sanitized_object_name
- @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
- end
+ def sanitized_object_name #:nodoc:
+ @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
+ 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)
+ def humanized_attribute_name(method) #:nodoc:
+ if @object && @object.class.respond_to?(:human_attribute_name) && @@label_str_method == :humanize
+ @object.class.human_attribute_name(method.to_s)
+ else
+ method.to_s.send(@@label_str_method)
+ end
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
+ # 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 titles/labels/hints/actions (titles = legends, actions = buttons).
+ #
+ def localized_string(key, value, type, options = {}) #:nodoc:
+ key = value if value.is_a?(::Symbol)
- 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
+ if value.is_a?(::String)
+ value
+ else
+ use_i18n = value.nil? ? @@i18n_lookups_by_default : (value != false)
+
+ if use_i18n
+ model_name = self.model_name.underscore
+ action_name = template.params[:action].to_s rescue ''
+ attribute_name = key.to_s
+
+ defaults = ::Formtastic::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 = ::Formtastic::I18n.t(defaults.shift,
+ options.merge(:default => defaults, :scope => type.to_s.pluralize.to_sym))
+ i18n_value.blank? ? nil : i18n_value
end
- defaults << ''
+ end
+ end
- i18n_value = ::I18n.t(defaults.shift, :default => defaults,
- :scope => "formtastic.#{i18n_key.to_s.pluralize}")
- i18n_value.blank? ? nil : i18n_value
+ def model_name
+ @object.present? ? @object.class.name : @object_name.to_s.classify
+ end
+
+ def send_or_call(duck, object)
+ if duck.is_a?(Proc)
+ duck.call(object)
+ else
+ object.send(duck)
end
end
- end
+ def set_include_blank(options)
+ unless options.key?(:include_blank) || options.key?(:prompt)
+ options[:include_blank] = @@include_blank_for_select_by_default
+ end
+ options
+ end
+
end
# Wrappers around form_for (etc) with :builder => SemanticFormBuilder.
#
# * semantic_form_for(@post)
@@ -1453,54 +1782,64 @@
# <%= f.input :title %>
# <%= f.input :body %>
# <% end %>
#
# The above examples use a resource-oriented style of form_for() helper where only the @post
- # object is given as an argument, but the generic style is also supported if you really want it,
- # as is forms with inline objects (Post.new) rather than objects with instance variables (@post):
+ # object is given as an argument, but the generic style is also supported, as are forms with
+ # inline objects (Post.new) rather than objects with instance variables (@post):
#
# <% semantic_form_for :post, @post, :url => posts_path do |f| %>
# ...
# <% end %>
#
# <% semantic_form_for :post, Post.new, :url => posts_path do |f| %>
# ...
# <% end %>
- #
- # The shorter, resource-oriented style is most definitely preferred, and has recieved the most
- # testing to date.
- #
- # Please note: Although it's possible to call Rails' built-in form_for() helper without an
- # object, all semantic forms *must* have an object (either Post.new or @post), as Formtastic
- # has too many dependencies on an ActiveRecord object being present.
- #
module SemanticFormHelper
- @@builder = Formtastic::SemanticFormBuilder
-
- # cattr_accessor :builder
- def self.builder=(val)
- @@builder = val
+ @@builder = ::Formtastic::SemanticFormBuilder
+ mattr_accessor :builder
+
+ @@default_field_error_proc = nil
+
+ # Override the default ActiveRecordHelper behaviour of wrapping the input.
+ # This gets taken care of semantically by adding an error class to the LI tag
+ # containing the input.
+ #
+ FIELD_ERROR_PROC = proc do |html_tag, instance_tag|
+ html_tag
end
-
- [:form_for, :fields_for, :form_remote_for, :remote_form_for].each do |meth|
+
+ def with_custom_field_error_proc(&block)
+ @@default_field_error_proc = ::ActionView::Base.field_error_proc
+ ::ActionView::Base.field_error_proc = FIELD_ERROR_PROC
+ result = yield
+ ::ActionView::Base.field_error_proc = @@default_field_error_proc
+ result
+ end
+
+ [:form_for, :fields_for, :remote_form_for].each do |meth|
src = <<-END_SRC
def semantic_#{meth}(record_or_name_or_array, *args, &proc)
options = args.extract_options!
- options[:builder] = @@builder
+ options[:builder] ||= @@builder
options[:html] ||= {}
-
+
class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
class_names << "formtastic"
class_names << case record_or_name_or_array
when String, Symbol then record_or_name_or_array.to_s # :post => "post"
when Array then record_or_name_or_array.last.class.to_s.underscore # [@post, @comment] # => "comment"
else record_or_name_or_array.class.to_s.underscore # @post => "post"
end
options[:html][:class] = class_names.join(" ")
-
- #{meth}(record_or_name_or_array, *(args << options), &proc)
+
+ with_custom_field_error_proc do
+ #{meth}(record_or_name_or_array, *(args << options), &proc)
+ end
end
END_SRC
module_eval src, __FILE__, __LINE__
end
+ alias :semantic_form_remote_for :semantic_remote_form_for
+
end
end