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