lib/formtastic.rb in nofxx-formtastic-0.1.6 vs lib/formtastic.rb in nofxx-formtastic-0.1.7

- old
+ new

@@ -33,11 +33,10 @@ :file => :file_field } STRING_MAPPINGS = [ :string, :password, :numeric ] attr_accessor :template - attr_writer :nested_child_index # 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. # # Options: @@ -45,18 +44,20 @@ # * :as - override the input type (eg force a :string to render as a :password field) # * :label - use something other than the method name as the label (or fieldset legend) text # * :required - specify if the column is required (true) or not (false) # * :hint - provide some text to hint or help the user provide the correct information for a field # * :input_html - provide options that will be passed down to the generated input + # * :wrapper_html - provide options that will be passed down to the li wrapper # # Input Types: # # Most inputs map directly to one of ActiveRecord's column types by default (eg string_input), # but there are a few special cases and some simplification (:integer, :float and :decimal # columns all map to a single numeric_input, for example). # # * :select (a select menu for associations) - default to association names + # * :time_zone (a select menu with time zones) # * :radio (a set of radio inputs for associations) - default to association names # * :password (a password input) - default for :string column types with 'password' in the method name # * :text (a textarea) - default for :text column types # * :date (a date select) - default for :date column types # * :datetime (a date and time select) - default for :datetime and :timestamp column types @@ -78,30 +79,26 @@ # def input(method, options = {}) options[:required] = method_required?(method, options[:required]) options[:as] ||= default_input_type(method) - options[:label] ||= if @object - @object.class.human_attribute_name(method.to_s) - else - method.to_s.send(@@label_str_method) - end + html_class = [ options[:as], (options[:required] ? :required : :optional) ] + html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors.on(method.to_s) + wrapper_html = options.delete(:wrapper_html) || {} + wrapper_html[:id] ||= generate_html_id(method) + wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ') + if [:boolean_select, :boolean_radio].include?(options[:as]) ::ActiveSupport::Deprecation.warn(":as => :#{options[:as]} is deprecated, use :as => :#{options[:as].to_s[8..-1]} instead", caller[3..-1]) end - html_class = [ options[:as], (options[:required] ? :required : :optional) ].join(' ') - html_class << ' error' if @object && @object.errors.on(method.to_s) - - html_id = generate_html_id(method) - list_item_content = @@inline_order.map do |type| send(:"inline_#{type}_for", method, options) end.compact.join("\n") - return template.content_tag(:li, list_item_content, { :id => html_id, :class => html_class }) + return template.content_tag(:li, list_item_content, wrapper_html) end # Creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be # called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc), # or with a list of fields. These two examples are functionally equivalent: @@ -231,25 +228,19 @@ # def inputs(*args, &block) html_options = args.extract_options! html_options[:class] ||= "inputs" - if fields_for_object = html_options.delete(:for) - html_options.merge!(:parent_builder => self) - inputs_for_nested_attributes(fields_for_object, args << html_options, - html_options.delete(:for_options) || {}, &block) + if html_options[:for] + inputs_for_nested_attributes(args, html_options, &block) elsif block_given? field_set_and_list_wrapping(html_options, &block) else if @object && args.empty? - # Get all belongs_to association args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to } - - # Get content columns and remove timestamps columns from it args += @object.class.content_columns.map(&:name) args -= %w[created_at updated_at created_on updated_on] - args.compact! end contents = args.map { |method| input(method.to_sym) } field_set_and_list_wrapping(html_options, contents) @@ -283,13 +274,14 @@ # # The value of the button text can be overridden: # # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" /> # - def commit_button(value=nil, options = {}) - value ||= save_or_create_button_text - template.content_tag(:li, template.submit_tag(value), :class => "commit") + def commit_button(value=nil, options={}) + value ||= save_or_create_button_text + button_html = options.delete(:button_html) || {} + template.content_tag(:li, self.submit(value, button_html), :class => "commit") end # A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder # for nesting forms: # @@ -321,11 +313,18 @@ # 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. You should add this on the partial + # + # Example: + # + # #contacts + # = f.input :phone + # = f.remove_link + # def remove_link(name, *args) options = args.extract_options! css_selector = options.delete(:selector) || ".#{@object.class.name.split("::").last.underscore}" function = options.delete(:function) || "" @@ -334,20 +333,26 @@ out = hidden_field(:_delete) out += template.link_to_function(name, function, *args.push(options)) end # Add a link to add more partials + # + # Example: + # + # - f.inputs "contacts" do + # = f.add_associated_link :contacts, :partial => "other/path" + # def add_associated_link(name, association, opts = {}) object = @object.send(association).build associated_name = extract_option_or_class_name(opts, :name, object) variable = "formtastic_next_#{associated_name}_id" opts.symbolize_keys! partial = opts.delete(:partial) || associated_name container = opts.delete(:expression) || "'#{opts.delete(:container) || '#'+associated_name.pluralize}'" - 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 = "if (typeof #{variable} == 'undefined') #{variable} = #{$1}; $(#{container}).append($.template(#{form.to_json}.replace(/__idx__/g, \"attributes_\" + @@ -361,19 +366,18 @@ 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] - 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 output = associated.map do |element| fields_for(association_name(name), element, (opts[:fields_for] || {}).merge(:name => name)) do |f| - template.render({:partial => "#{partial}", :locals => {local_assign_name.to_sym => element, :f => f}.merge(opts[:locals] || {})}.merge(opts[:render] || {})) + render({:partial => "#{partial}", :locals => {local_assign_name.to_sym => element, :f => f}.merge(opts[:locals] || {})}.merge(opts[:render] || {})) end end output.join end end @@ -395,21 +399,24 @@ # 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(fields_for_object, inputs, options, &block) + 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(*inputs){ block.call(f) } } + proc { |f| f.inputs(*args){ block.call(f) } } else - proc { |f| f.inputs(*inputs) } + proc { |f| f.inputs(*args) } end - semantic_fields_for(*(Array(fields_for_object) << options), &fields_for_block) + fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten + semantic_fields_for(*fields_for_args, &fields_for_block) end # Remove any Formtastic-specific options before passing the down options. # def set_options(options) @@ -566,10 +573,24 @@ input_label(input_name, options.delete(:label), options.slice(:required)) + self.select(input_name, collection, set_options(options), html_options) end alias :boolean_select_input :select_input + # Outputs a timezone select input as Rails' time_zone_select helper. You + # can give priority zones as option. + # + # Examples: + # + # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/ + # + def time_zone_input(method, options) + html_options = options.delete(:input_html) || {} + + input_label(method, options.delete(:label), options.slice(:required)) + + self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options) + end + # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list # items, one for each possible choice in the belongs_to association. Each li contains a # label and a radio input. # # Example: @@ -746,15 +767,17 @@ # :checked_value and :unchecked_value options are also available. # def boolean_input(method, options) html_options = options.delete(:input_html) || {} - content = self.check_box(method, set_options(options).merge(html_options), - options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0') + input = self.check_box(method, set_options(options).merge(html_options), + options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0') - # required does not make sense in check box - input_label(method, content + options.delete(:label), :skip_required => true) + # Generate the label by hand because required or optional does not make sense here + label = options.delete(:label) || humanized_attribute_name(method) + + self.label(method, input + label, options) end # Generates an input for the given method using the type supplied with :as. # # If the input is included in INPUT_MAPPINGS, it uses input_simple @@ -774,20 +797,36 @@ # Generates error messages for the given method. Errors can be shown as list # or as sentence. If :none is set, no error is shown. # def inline_errors_for(method, options) #:nodoc: - return nil unless @object && [:sentence, :list].include?(@@inline_errors) + return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors) - errors = @object.errors.on(method.to_s).to_a + # Ruby 1.9: Strings are not Enumerable, ie no String#to_a + errors = @object.errors.on(method.to_s) + unless errors.respond_to?(:to_a) + errors = [errors] + else + errors = errors.to_a + end + + # Ruby 1.9: Strings are not Enumerable, ie no String#to_a + errors = @object.errors.on(method.to_s) + unless errors.respond_to?(:to_a) + errors = [errors] + else + errors = errors.to_a + end + send("error_#{@@inline_errors}", errors) unless errors.empty? end # Generates hints for the given method using the text supplied in :hint. # def inline_hints_for(method, options) #:nodoc: - options[:hint].blank? ? '' : template.content_tag(:p, options[:hint], :class => 'inline-hints') + return if options[:hint].blank? + template.content_tag(:p, options[:hint], :class => 'inline-hints') end # Creates an error sentence by calling to_sentence on the errors array. # def error_sentence(errors) #:nodoc: @@ -807,11 +846,12 @@ # Generates the label for the input. Accepts the same options as Rails label # method and a fourth option that allows the label to be generated as span # with class label. # def input_label(method, text, options={}, as_span=false) #:nodoc: - text << required_or_optional_string(options.delete(:required)) unless options.delete(:skip_required) + text ||= humanized_attribute_name(method) + text << required_or_optional_string(options.delete(:required)) if as_span options[:class] ||= 'label' template.content_tag(:span, text, options) else @@ -839,27 +879,22 @@ # 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. # - # If you are using inputs :for, for more than one association in the same - # form builder, you might want to set the nested_child_index as well. You - # can do that doing: - # - # f.nested_child_index = -1 - # - def field_set_and_list_wrapping(html_options, contents = '', &block) #:nodoc: - # Generates the legend text allowing nested_child_index support for interpolation - legend_text = html_options.delete(:name).to_s - legend_text %= html_options[:parent_builder].instance_variable_get('@nested_child_index').to_i + 1 + 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? - legend = legend_text.blank? ? "" : template.content_tag(:legend, template.content_tag(:span, legend_text)) contents = template.capture(&block) if block_given? + # 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_builder) + html_options.except(:builder, :parent) ) template.concat(fieldset) if block_given? fieldset end @@ -872,38 +907,37 @@ %{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} + template.content_tag(:ol, contents) ) end - # For methods that have a database column, take a best guess as to what the inout method + # For methods that have a database column, take a best guess as to what the input method # should be. In most cases, it will just return the column type (eg :string), but for special # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do # something different (like :password and :select). # # If there is no column for the method (eg "virtual columns" with an attr_accessor), the # default is a :string, a similar behaviour to Rails' scaffolding. # def default_input_type(method) #:nodoc: return :string if @object.nil? - # Find the column object by attribute column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute) - # Associations map by default to a select - return :select if column.nil? && find_reflection(method) - if column # handle the special cases where the column type doesn't map to an input method - return :select if column.type == :integer && method.to_s =~ /_id$/ - return :datetime if column.type == :timestamp - return :numeric if [:integer, :float, :decimal].include?(column.type) - return :password if column.type == :string && method.to_s =~ /password/ + return :time_zone if column.type == :string && method.to_s =~ /time_?zone/ + return :select if column.type == :integer && method.to_s =~ /_id$/ + return :datetime if column.type == :timestamp + return :numeric if [:integer, :float, :decimal].include?(column.type) + return :password if column.type == :string && method.to_s =~ /password/ + # otherwise assume the input name will be the same as the column type (eg string_input) return column.type else obj = @object.send(method) if @object.respond_to?(method) + return :select if find_reflection(method) return :file if obj && @@file_methods.any? { |m| obj.respond_to?(m) } return :password if method.to_s =~ /password/ return :string end end @@ -984,11 +1018,11 @@ # If an association method is passed in (f.input :author) try to find the # reflection object. # def find_reflection(method) - object.class.reflect_on_association(method) if object.class.respond_to?(:reflect_on_association) + @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association) end # Generates default_string_options by retrieving column information from # the database. # @@ -1018,11 +1052,35 @@ sanitized_method_name = method_name.to_s.sub(/\?$/,"") "#{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') + + 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 + def sanitized_object_name @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) + end end end