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