lib/formtastic.rb in nofxx-formtastic-0.1.8 vs lib/formtastic.rb in nofxx-formtastic-0.1.9

- old
+ new

@@ -16,17 +16,17 @@ @@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"] cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string, :optional_string, :inline_errors, :label_str_method, :collection_label_methods, - :inline_order, :file_methods + :inline_order, :file_methods, :priority_countries # Keeps simple mappings in a hash - # INPUT_MAPPINGS = { :string => :text_field, :password => :password_field, :numeric => :text_field, :text => :text_area, @@ -40,11 +40,11 @@ # and other factors (like the method name) to figure out what you probably want. # # Options: # # * :as - override the input type (eg force a :string to render as a :password field) - # * :label - use something other than the method name as the label (or fieldset legend) text + # * :label - use something other than the method name as the label text, when false no label is printed # * :required - specify if the column is required (true) or not (false) # * :hint - provide some text to hint or help the user provide the correct information for a field # * :input_html - provide options that will be passed down to the generated input # * :wrapper_html - provide options that will be passed down to the li wrapper # @@ -53,20 +53,23 @@ # Most inputs map directly to one of ActiveRecord's column types by default (eg string_input), # but there are a few special cases and some simplification (:integer, :float and :decimal # columns all map to a single numeric_input, for example). # # * :select (a select menu for associations) - default to association names + # * :check_boxes (a set of check_box inputs for associations) - alternative to :select has_many and has_and_belongs_to_many associations + # * :radio (a set of radio inputs for associations) - alternative to :select belongs_to associations # * :time_zone (a select menu with time zones) - # * :radio (a set of radio inputs for associations) - default to association names # * :password (a password input) - default for :string column types with 'password' in the method name # * :text (a textarea) - default for :text column types # * :date (a date select) - default for :date column types # * :datetime (a date and time select) - default for :datetime and :timestamp column types # * :time (a time select) - default for :time column types # * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio) # * :string (a text field) - default for :string column types # * :numeric (a text field, like string) - default for :integer, :float and :decimal column types + # * :country (a select menu of country names) - requires a country_select plugin to be installed + # * :hidden (a hidden field) - creates a hidden field (added for compatibility) # # Example: # # <% semantic_form_for @employee do |form| %> # <% form.inputs do -%> @@ -76,11 +79,11 @@ # <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %> # <% end %> # <% end %> # def input(method, options = {}) - options[:required] = method_required?(method, options[:required]) + options[:required] = method_required?(method) unless options.key?(:required) options[:as] ||= default_input_type(method) html_class = [ options[:as], (options[:required] ? :required : :optional) ] html_class << 'error' if @object && @object.respond_to?(:errors) && @object.errors.on(method.to_s) @@ -306,11 +309,70 @@ opts.merge!(:builder => Formtastic::SemanticFormBuilder) args.push(opts) fields_for(record_or_name_or_array, *args, &block) end + # Generates the label for the input. It also accepts the same arguments as + # Rails label method. It has three options that are not supported by Rails + # label method: # + # * :required - Appends an abbr tag if :required is true + # * :label - An alternative form to give the label content. Whenever label + # is false, a blank string is returned. + # * :as_span - When true returns a span tag with class label instead of a label element + # + # == 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 ||= humanized_attribute_name(method) + text << required_or_optional_string(options.delete(:required)) + + if options.delete(:as_span) + options[:class] ||= 'label' + template.content_tag(:span, text, options) + else + super(method, 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.on(method.to_s) + 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. # @@ -452,13 +514,11 @@ # otherwise. # # * if the :required option isn't provided, and the plugin isn't available, the value of the # configuration option @@all_fields_required_by_default is used. # - def method_required?(attribute, required_option) #:nodoc: - return required_option unless required_option.nil? - + def method_required?(attribute) #:nodoc: if @object && @object.class.respond_to?(:reflect_on_all_validations) attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym @object.class.reflect_on_all_validations.any? do |validation| validation.macro == :validates_presence_of && validation.name == attribute_sym @@ -470,19 +530,30 @@ # 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. + # If input_html is given as option, it's passed down to the input. The :size option is + # also passed in since it's so common to want to customize it. # def input_simple(type, method, options) html_options = options.delete(:input_html) || {} html_options = default_string_options(method).merge(html_options) if STRING_MAPPINGS.include?(type) + html_options[:size] = options.delete(:size) if options.has_key?(:size) - input_label(method, options.delete(:label), options.slice(:required)) + send(INPUT_MAPPINGS[type], method, html_options) + self.label(method, options.slice(:label, :required)) + + self.send(INPUT_MAPPINGS[type], method, html_options) end + # Outputs a hidden field inside the wrapper, which should be hidden with CSS. + # Additionals options can be given and will be sent straight to hidden input + # element. + # + def hidden_input(method, options) + self.hidden_field(method, set_options(options)) + end + # Outputs a label and a select box containing options from the parent # (belongs_to, has_many, has_and_belongs_to_many) association. If an association # is has_many or has_and_belongs_to_many the select box will be set as multi-select # and size = 5 # @@ -568,11 +639,11 @@ html_options[:multiple] ||= true html_options[:size] ||= 5 end input_name = generate_association_input_name(method) - input_label(input_name, options.delete(:label), options.slice(:required)) + + self.label(input_name, options.slice(:label, :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 @@ -583,11 +654,11 @@ # 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.label(method, options.slice(:label, :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 @@ -612,11 +683,11 @@ # </fieldset> # # You can customize the options available in the set by passing in a collection (Array) of # ActiveRecord objects through the :collection option. If not provided, the choices are found # by inferring the parent's class name from the method name and simply calling find(:all) on - # it (VehicleOwner.find(:all) in the example above). + # 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) @@ -730,54 +801,160 @@ time_inputs = [:hour, :minute] time_inputs << [:second] if options[:include_seconds] list_items_capture = "" + hidden_fields_capture = "" # Gets the datetime object. It can be a Fixnum, Date or Time, or nil. datetime = @object ? @object.send(method) : nil html_options = options.delete(:input_html) || {} (inputs + time_inputs).each do |input| html_id = generate_html_id(method, "#{position[input]}i") field_name = "#{method}(#{position[input]}i)" - - list_items_capture << if options["discard_#{input}".intern] + if options["discard_#{input}".intern] break if time_inputs.include?(input) - + hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime - template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id) + hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id) else opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name) item_label_text = I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts]) - template.content_tag(:li, + list_items_capture << template.content_tag(:li, template.content_tag(:label, item_label_text, :for => html_id) + template.send("select_#{input}".intern, datetime, opts, html_options.merge(:id => html_id)) ) end end - field_set_and_list_wrapping_for_method(method, options, list_items_capture) + hidden_fields_capture + field_set_and_list_wrapping_for_method(method, options, list_items_capture) end + + # 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) || {} + + input_name = generate_association_input_name(method) + value_as_class = options.delete(:value_as_class) + unchecked_value = options.delete(:unchecked_value) || '' + html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options) + + list_item_content = collection.map do |c| + label = c.is_a?(Array) ? c.first : c + value = c.is_a?(Array) ? c.last : c + + 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] + ) + + li_options = value_as_class ? { :class => value.to_s.downcase } : {} + template.content_tag(:li, li_content, li_options) + end + + field_set_and_list_wrapping_for_method(method, options, list_item_content) + end + + + # Outputs a country select input, wrapping around a regular country_select helper. + # Rails doesn't come with a country_select helper by default any more, so you'll need to install + # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the + # same way. + # + # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select. + # + # By default, Formtastic includes a handfull of english-speaking countries as "priority counties", + # which you can change to suit your market and user base (see README for more info on config). + # + # Examples: + # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries + # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own + # + def country_input(method, options) + raise "To use the :country input, please install a country_select plugin, like this one: http://github.com/rails/iso-3166-country-select" unless self.respond_to?(:country_select) + + html_options = options.delete(:input_html) || {} + priority_countries = options.delete(:priority_countries) || @@priority_countries + + self.label(method, options.slice(:label, :required)) + + self.country_select(method, priority_countries, set_options(options), html_options) + end + + # Outputs a label containing a checkbox and the label text. The label defaults # to the column name (method name) and can be altered with the :label option. - # - # Different from other inputs, :required options has no effect here and # :checked_value and :unchecked_value options are also available. # def boolean_input(method, options) html_options = options.delete(:input_html) || {} input = self.check_box(method, set_options(options).merge(html_options), options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0') - # 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) + self.label(method, input + label, options.slice(:required)) 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 @@ -793,84 +970,50 @@ else send("#{input_type}_input", method, options) end end - # Generates error messages for the given method. Errors can be shown as list - # or as sentence. If :none is set, no error is shown. - # - def inline_errors_for(method, options) #:nodoc: - return nil unless @object && @object.respond_to?(:errors) && [:sentence, :list].include?(@@inline_errors) - - # Ruby 1.9: Strings are not Enumerable, ie no String#to_a - errors = @object.errors.on(method.to_s) - unless errors.respond_to?(:to_a) - errors = [errors] - else - errors = errors.to_a - end - - # 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: return if options[:hint].blank? template.content_tag(:p, options[:hint], :class => 'inline-hints') end # Creates an error sentence by calling to_sentence on the errors array. # def error_sentence(errors) #:nodoc: - template.content_tag(:p, errors.to_sentence, :class => 'inline-errors') + template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors') end # Creates an error li list. # def error_list(errors) #:nodoc: list_elements = [] errors.each do |error| - list_elements << template.content_tag(:li, error) + list_elements << template.content_tag(:li, error.untaint) end template.content_tag(:ul, list_elements.join("\n"), :class => 'errors') end - # Generates the label for the input. Accepts the same options as Rails label - # method and a fourth option that allows the label to be generated as span - # with class label. - # - def input_label(method, text, options={}, as_span=false) #:nodoc: - text ||= 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 - self.label(method, text, options) - end - end - # Generates the required or optional string. If the value set is a proc, # it evaluates the proc first. # def required_or_optional_string(required) #:nodoc: - string_or_proc = required ? @@required_string : @@optional_string + string_or_proc = case required + when true + @@required_string + when false + @@optional_string + else + required + end - if string_or_proc.is_a? Proc + if string_or_proc.is_a?(Proc) string_or_proc.call else - string_or_proc + string_or_proc.to_s end end # Generates a fieldset and wraps the content in an ordered list. When working # with nested attributes (in Rails 2.3), it allows %i as interpolation option @@ -902,11 +1045,11 @@ # 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) template.content_tag(:fieldset, - %{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} + + %{<legend>#{self.label(method, options.slice(:label, :required).merge!(:as_span => true))}</legend>} + template.content_tag(:ol, contents) ) end # For methods that have a database column, take a best guess as to what the input method @@ -922,14 +1065,15 @@ column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute) 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 :time_zone if column.type == :string && method.to_s =~ /time_zone/ return :select if column.type == :integer && method.to_s =~ /_id$/ return :datetime if column.type == :timestamp return :numeric if [:integer, :float, :decimal].include?(column.type) return :password if column.type == :string && method.to_s =~ /password/ + return :country if column.type == :string && method.to_s =~ /country/ # otherwise assume the input name will be the same as the column type (eg string_input) return column.type else obj = @object.send(method) if @object.respond_to?(method)