justinfrench-formtastic 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/lib/formtastic.rb CHANGED
@@ -14,14 +14,28 @@ module Formtastic #:nodoc:
14
14
  @@required_string = proc { %{<abbr title="#{I18n.t 'formtastic.required', :default => 'required'}">*</abbr>} }
15
15
  @@optional_string = ''
16
16
  @@inline_errors = :sentence
17
- @@label_str_method = :to_s
17
+ @@label_str_method = :humanize
18
18
  @@collection_label_methods = %w[to_label display_name full_name name title username login value to_s]
19
19
  @@inline_order = [ :input, :hints, :errors ]
20
+ @@file_methods = [ :file?, :public_filename ]
20
21
 
21
- 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
22
+ cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string,
23
+ :optional_string, :inline_errors, :label_str_method, :collection_label_methods,
24
+ :inline_order, :file_methods
22
25
 
23
- attr_accessor :template
26
+ # Keeps simple mappings in a hash
27
+ #
28
+ INPUT_MAPPINGS = {
29
+ :string => :text_field,
30
+ :password => :password_field,
31
+ :numeric => :text_field,
32
+ :text => :text_area,
33
+ :file => :file_field
34
+ }
35
+ STRING_MAPPINGS = [ :string, :password, :numeric ]
24
36
 
37
+ attr_accessor :template
38
+ attr_writer :nested_child_index
25
39
 
26
40
  # Returns a suitable form input for the given +method+, using the database column information
27
41
  # and other factors (like the method name) to figure out what you probably want.
@@ -32,6 +46,7 @@ module Formtastic #:nodoc:
32
46
  # * :label - use something other than the method name as the label (or fieldset legend) text
33
47
  # * :required - specify if the column is required (true) or not (false)
34
48
  # * :hint - provide some text to hint or help the user provide the correct information for a field
49
+ # * :input_html - provide options that will be passed down to the generated input
35
50
  #
36
51
  # Input Types:
37
52
  #
@@ -39,15 +54,14 @@ module Formtastic #:nodoc:
39
54
  # but there are a few special cases and some simplification (:integer, :float and :decimal
40
55
  # columns all map to a single numeric_input, for example).
41
56
  #
42
- # * :select (a select menu for belongs_to associations) - default for columns ending in '_id'
43
- # * :radio (a set of radio inputs for belongs_to associations) - alternative for columns ending in '_id'
57
+ # * :select (a select menu for associations) - default to association names
58
+ # * :radio (a set of radio inputs for associations) - default to association names
44
59
  # * :password (a password input) - default for :string column types with 'password' in the method name
45
60
  # * :text (a textarea) - default for :text column types
46
61
  # * :date (a date select) - default for :date column types
47
62
  # * :datetime (a date and time select) - default for :datetime and :timestamp column types
48
63
  # * :time (a time select) - default for :time column types
49
- # * :boolean (a checkbox) - default for :boolean column types
50
- # * :boolean_select (a yes/no select box)
64
+ # * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio)
51
65
  # * :string (a text field) - default for :string column types
52
66
  # * :numeric (a text field, like string) - default for :integer, :float and :decimal column types
53
67
  #
@@ -61,29 +75,28 @@ module Formtastic #:nodoc:
61
75
  # <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %>
62
76
  # <% end %>
63
77
  # <% end %>
78
+ #
64
79
  def input(method, options = {})
65
- raise NoMethodError.new("NoMethodError: form object does not respond to \"#{method}\"") unless @object.respond_to?(method)
80
+ options[:required] = method_required?(method, options[:required])
81
+ options[:as] ||= default_input_type(method)
66
82
 
83
+ options[:label] ||= if @object
84
+ @object.class.human_attribute_name(method.to_s)
85
+ else
86
+ method.to_s.send(@@label_str_method)
87
+ end
67
88
 
68
- options[:required] = method_required?(method, options[:required])
69
- options[:label] ||= @object.class.human_attribute_name(method.to_s).send(@@label_str_method)
70
- options[:as] ||= default_input_type(@object, method)
71
- input_method = "#{options[:as]}_input"
89
+ if [:boolean_select, :boolean_radio].include?(options[:as])
90
+ ::ActiveSupport::Deprecation.warn(":as => :#{options[:as]} is deprecated, use :as => :#{options[:as].to_s[8..-1]} instead", caller[3..-1])
91
+ end
72
92
 
73
- html_class = [
74
- options[:as].to_s,
75
- (options[:required] ? 'required' : 'optional'),
76
- (@object.errors.on(method.to_s) ? 'error' : nil)
77
- ].compact.join(" ")
93
+ html_class = [ options[:as], (options[:required] ? :required : :optional) ].join(' ')
94
+ html_class << ' error' if @object && @object.errors.on(method.to_s)
78
95
 
79
96
  html_id = generate_html_id(method)
80
97
 
81
98
  list_item_content = @@inline_order.map do |type|
82
- if type == :input
83
- send(input_method, method, options)
84
- else
85
- send(:"inline_#{type}", method, options)
86
- end
99
+ send(:"inline_#{type}_for", method, options)
87
100
  end.compact.join("\n")
88
101
 
89
102
  return template.content_tag(:li, list_item_content, { :id => html_id, :class => html_class })
@@ -221,20 +234,28 @@ module Formtastic #:nodoc:
221
234
  html_options[:class] ||= "inputs"
222
235
 
223
236
  if fields_for_object = html_options.delete(:for)
224
- inputs_for_nested_attributes(fields_for_object, args << html_options, &block)
237
+ html_options.merge!(:parent_builder => self)
238
+ inputs_for_nested_attributes(fields_for_object, args << html_options,
239
+ html_options.delete(:for_options) || {}, &block)
225
240
  elsif block_given?
226
241
  field_set_and_list_wrapping(html_options, &block)
227
242
  else
228
- if args.empty?
229
- args = @object.class.reflections.map { |n,_| n }
243
+ if @object && args.empty?
244
+ # Get all belongs_to association
245
+ args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to }
246
+
247
+ # Get content columns and remove timestamps columns from it
230
248
  args += @object.class.content_columns.map(&:name)
249
+ args -= %w[created_at updated_at created_on updated_on]
250
+
251
+ args.compact!
231
252
  end
232
253
  contents = args.map { |method| input(method.to_sym) }
233
254
 
234
255
  field_set_and_list_wrapping(html_options, contents)
235
256
  end
236
257
  end
237
- alias_method :input_field_set, :inputs
258
+ alias :input_field_set :inputs
238
259
 
239
260
  # Creates a fieldset and ol tag wrapping for form buttons / actions as list items.
240
261
  # See inputs documentation for a full example. The fieldset's default class attriute
@@ -253,7 +274,7 @@ module Formtastic #:nodoc:
253
274
  field_set_and_list_wrapping(html_options, contents)
254
275
  end
255
276
  end
256
- alias_method :button_field_set, :buttons
277
+ alias :button_field_set :buttons
257
278
 
258
279
  # Creates a submit input tag with the value "Save [model name]" (for existing records) or
259
280
  # "Create [model name]" (for new records) by default:
@@ -263,11 +284,12 @@ module Formtastic #:nodoc:
263
284
  # The value of the button text can be overridden:
264
285
  #
265
286
  # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
266
- def commit_button(value = save_or_create_commit_button_text, options = {})
287
+ #
288
+ def commit_button(value=nil, options = {})
289
+ value ||= save_or_create_button_text
267
290
  template.content_tag(:li, template.submit_tag(value), :class => "commit")
268
291
  end
269
292
 
270
-
271
293
  # A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
272
294
  # for nesting forms:
273
295
  #
@@ -286,6 +308,7 @@ module Formtastic #:nodoc:
286
308
  # </ol>
287
309
  # </fieldset>
288
310
  # </form>
311
+ #
289
312
  def semantic_fields_for(record_or_name_or_array, *args, &block)
290
313
  opts = args.extract_options!
291
314
  opts.merge!(:builder => Formtastic::SemanticFormBuilder)
@@ -295,54 +318,68 @@ module Formtastic #:nodoc:
295
318
 
296
319
  protected
297
320
 
298
- # Deals with :for option when it's supplied to inputs methods.
321
+ # Deals with :for option when it's supplied to inputs methods. Additional
322
+ # options to be passed down to :for should be supplied using :for_options
323
+ # key.
324
+ #
299
325
  # It should raise an error if a block with arity zero is given.
300
326
  #
301
- def inputs_for_nested_attributes(fields_for_object, inputs_args, &block)
327
+ def inputs_for_nested_attributes(fields_for_object, inputs, options, &block)
302
328
  fields_for_block = if block_given?
303
329
  raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
304
330
  'but the block does not accept any argument.' if block.arity <= 0
305
331
 
306
- proc { |f| f.inputs(*inputs_args){ block.call(f) } }
332
+ proc { |f| f.inputs(*inputs){ block.call(f) } }
307
333
  else
308
- proc { |f| f.inputs(*inputs_args) }
334
+ proc { |f| f.inputs(*inputs) }
309
335
  end
310
336
 
311
- semantic_fields_for(*Array(fields_for_object), &fields_for_block)
337
+ semantic_fields_for(*(Array(fields_for_object) << options), &fields_for_block)
312
338
  end
313
339
 
314
- # Ensure :object => @object is set before sending the options down to the Rails layer.
315
- # Also remove any Formtastic-specific options
340
+ # Remove any Formtastic-specific options before passing the down options.
341
+ #
316
342
  def set_options(options)
317
- opts = options.dup
318
- [:value_method, :label_method, :collection, :required, :label, :as, :hint].each do |key|
319
- opts.delete(key)
320
- end
321
- opts.merge(:object => @object)
343
+ options.except(:value_method, :label_method, :collection, :required, :label,
344
+ :as, :hint, :input_html, :label_html, :value_as_class)
322
345
  end
323
346
 
324
- def save_or_create_commit_button_text #:nodoc:
325
- prefix = @object.new_record? ? 'create' : 'save'
326
- [ I18n.t(prefix, :default => prefix, :scope => [:formtastic]),
327
- @object.class.human_name
328
- ].join(' ').send(@@label_str_method)
347
+ # Create a default button text. If the form is working with a object, it
348
+ # defaults to "Create model" or "Save model" depending if we are working
349
+ # with a new_record or not.
350
+ #
351
+ # When not working with models, it defaults to "Submit object".
352
+ #
353
+ def save_or_create_button_text(prefix='Submit') #:nodoc:
354
+ if @object
355
+ prefix = @object.new_record? ? 'Create' : 'Save'
356
+ object_name = @object.class.human_name
357
+ else
358
+ object_name = @object_name.to_s.send(@@label_str_method)
359
+ end
360
+
361
+ I18n.t(prefix.downcase, :default => prefix, :scope => [:formtastic]) << ' ' << object_name
329
362
  end
330
363
 
331
364
  # Determins if the attribute (eg :title) should be considered required or not.
332
365
  #
333
366
  # * if the :required option was provided in the options hash, the true/false value will be
334
367
  # returned immediately, allowing the view to override any guesswork that follows:
368
+ #
335
369
  # * if the :required option isn't provided in the options hash, and the ValidationReflection
336
370
  # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
337
371
  # if the validates_presence_of macro has been used in the class for this attribute, or false
338
372
  # otherwise.
373
+ #
339
374
  # * if the :required option isn't provided, and the plugin isn't available, the value of the
340
375
  # configuration option @@all_fields_required_by_default is used.
376
+ #
341
377
  def method_required?(attribute, required_option) #:nodoc:
342
378
  return required_option unless required_option.nil?
343
379
 
344
- if @object.class.respond_to?(:reflect_on_all_validations)
380
+ if @object && @object.class.respond_to?(:reflect_on_all_validations)
345
381
  attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
382
+
346
383
  @object.class.reflect_on_all_validations.any? do |validation|
347
384
  validation.macro == :validates_presence_of && validation.name == attribute_sym
348
385
  end
@@ -351,6 +388,19 @@ module Formtastic #:nodoc:
351
388
  end
352
389
  end
353
390
 
391
+ # A method that deals with most of inputs (:string, :password, :file,
392
+ # :textarea and :numeric). :select, :radio, :boolean and :datetime inputs
393
+ # are not handled by this method, since they need more detailed approach.
394
+ #
395
+ # If input_html is given as option, it's passed down to the input.
396
+ #
397
+ def input_simple(type, method, options)
398
+ html_options = options.delete(:input_html) || {}
399
+ html_options = default_string_options(method).merge(html_options) if STRING_MAPPINGS.include?(type)
400
+
401
+ input_label(method, options.delete(:label), options.slice(:required)) + send(INPUT_MAPPINGS[type], method, html_options)
402
+ end
403
+
354
404
  # Outputs a label and a select box containing options from the parent
355
405
  # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
356
406
  # is has_many or has_and_belongs_to_many the select box will be set as multi-select
@@ -427,32 +477,23 @@ module Formtastic #:nodoc:
427
477
  #
428
478
  # Examples:
429
479
  #
430
- # f.input :authors, :html => {:size => 20, :multiple => true}
480
+ # f.input :authors, :input_html => {:size => 20, :multiple => true}
481
+ #
431
482
  def select_input(method, options)
432
- options[:collection] ||= find_parent_objects_for_column(method)
433
- options[:label_method] ||= detect_label_method(options[:collection])
434
- options[:value_method] ||= :id
435
- options[:input_html] ||= {}
436
-
437
- if (reflection = find_reflection(method)) && reflection.macro != :belongs_to
438
- options[:input_html][:multiple] ||= true
439
- options[:input_html][:size] ||= 5
440
- end
483
+ collection = find_collection_for_column(method, options)
484
+ html_options = options.delete(:input_html) || {}
441
485
 
442
- input_name = generate_association_input_name(method)
443
- html_options = options.delete(:input_html)
444
- choices = formatted_collection(options[:collection], options[:label_method], options[:value_method])
445
- input_label(input_name, options) + template.select(@object_name, input_name, choices, set_options(options), html_options)
446
- end
486
+ reflection = find_reflection(method)
487
+ if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
488
+ html_options[:multiple] ||= true
489
+ html_options[:size] ||= 5
490
+ end
447
491
 
448
- def detect_label_method(collection) #:nodoc:
449
- (!collection.instance_of?(Hash)) ? @@collection_label_methods.detect { |m| collection.first.respond_to?(m) } : nil
450
- end
451
-
452
- def formatted_collection(collection, label_method, value_method = :id) #:nodoc:
453
- return collection if (collection.instance_of?(Hash) || (collection.instance_of?(Array) && collection.first.instance_of?(String)))
454
- collection.map { |o| [o.send(label_method), o.send(value_method)] }
492
+ input_name = generate_association_input_name(method)
493
+ input_label(input_name, options.delete(:label), options.slice(:required)) +
494
+ self.select(input_name, collection, set_options(options), html_options)
455
495
  end
496
+ alias :boolean_select_input :select_input
456
497
 
457
498
  # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
458
499
  # items, one for each possible choice in the belongs_to association. Each li contains a
@@ -500,69 +541,31 @@ module Formtastic #:nodoc:
500
541
  # f.input :author, :as => :radio, :label_method => :label
501
542
  #
502
543
  # Finally, you can set :value_as_class => true if you want that LI wrappers
503
- # contains a class with the wrapped radio input value. This is used by
504
- # <tt>boolean_radio_input</tt> and you can see an example there.
544
+ # contains a class with the wrapped radio input value.
505
545
  #
506
546
  def radio_input(method, options)
507
- options[:collection] ||= find_parent_objects_for_column(method)
508
- options[:label_method] ||= detect_label_method(options[:collection])
547
+ collection = find_collection_for_column(method, options)
548
+ html_options = set_options(options).merge(options.delete(:input_html) || {})
509
549
 
510
550
  input_name = generate_association_input_name(method)
511
551
  value_as_class = options.delete(:value_as_class)
512
552
 
513
- choices = formatted_collection(options[:collection], options[:label_method])
514
- template.content_tag(:fieldset,
515
- %{<legend><span>#{label_text(method, options)}</span></legend>} +
516
- template.content_tag(:ol,
517
- choices.map { |c|
518
- label = (!c.instance_of?(String)) ? c.first : c
519
- value = (!c.instance_of?(String)) ? c.last : c
520
-
521
- li_content = template.content_tag(:label,
522
- "#{template.radio_button(@object_name, input_name, value, set_options(options))} #{label}",
523
- :for => generate_html_id(input_name, value.to_s.downcase)
524
- )
525
-
526
- li_options = value_as_class ? { :class => value.to_s.downcase } : {}
527
- template.content_tag(:li, li_content, li_options)
528
- }
529
- )
530
- )
531
- end
532
-
533
- # Outputs a label and a password input, nothing fancy.
534
- def password_input(method, options)
535
- input_label(method, options) +
536
- template.password_field(@object_name, method, default_string_options(method))
537
- end
538
-
539
-
540
- # Outputs a label and a textarea, nothing fancy.
541
- def text_input(method, options)
542
- input_label(method, options) + template.text_area(@object_name, method, set_options(options))
543
- end
544
-
545
-
546
- # Outputs a label and a text input, nothing fancy, but it does pick up some attributes like
547
- # size and maxlength -- see default_string_options() for the low-down.
548
- def string_input(method, options)
549
- input_label(method, options) +
550
- template.text_field(@object_name, method, default_string_options(method))
551
- end
553
+ list_item_content = collection.map do |c|
554
+ label = c.is_a?(Array) ? c.first : c
555
+ value = c.is_a?(Array) ? c.last : c
552
556
 
557
+ li_content = template.content_tag(:label,
558
+ "#{self.radio_button(input_name, value, html_options)} #{label}",
559
+ :for => generate_html_id(input_name, value.to_s.downcase)
560
+ )
553
561
 
554
- # Same as string_input for now
555
- def numeric_input(method, options)
556
- input_label(method, options) +
557
- template.text_field(@object_name, method, default_string_options(method))
558
- end
562
+ li_options = value_as_class ? { :class => value.to_s.downcase } : {}
563
+ template.content_tag(:li, li_content, li_options)
564
+ end
559
565
 
560
- # Outputs label and file field
561
- def file_input(method, options)
562
- input_label(method, options) +
563
- template.file_field(@object_name, method, set_options(options))
566
+ field_set_and_list_wrapping_for_method(method, options, list_item_content)
564
567
  end
565
-
568
+ alias :boolean_radio_input :radio_input
566
569
 
567
570
  # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
568
571
  # items (li), one for each fragment for the date (year, month, day). Each li contains a label
@@ -629,118 +632,97 @@ module Formtastic #:nodoc:
629
632
  #
630
633
  def date_or_datetime_input(method, options)
631
634
  position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
632
- inputs = options.delete(:order) || I18n.translate(:'date.order') || [:year, :month, :day]
635
+ inputs = options.delete(:order) || I18n.translate(:'date.order') || [:year, :month, :day]
636
+
633
637
  time_inputs = [:hour, :minute]
634
638
  time_inputs << [:second] if options[:include_seconds]
635
639
 
640
+ list_items_capture = ""
641
+
636
642
  # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
637
- datetime = @object.send(method)
643
+ datetime = @object ? @object.send(method) : nil
644
+ html_options = options.delete(:input_html) || {}
638
645
 
639
- list_items_capture = ""
640
646
  (inputs + time_inputs).each do |input|
641
- html_id = generate_html_id(method, "#{position[input]}i")
647
+ html_id = generate_html_id(method, "#{position[input]}i")
648
+ field_name = "#{method}(#{position[input]}i)"
642
649
 
643
- if options["discard_#{input}".intern]
650
+ list_items_capture << if options["discard_#{input}".intern]
644
651
  break if time_inputs.include?(input)
652
+
645
653
  hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime
646
- list_items_capture << template.hidden_field_tag("#{@object_name}[#{method}(#{position[input]}i)]", (hidden_value || 1), :id => html_id)
654
+ template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id)
647
655
  else
648
- opts = set_options(options).merge(:prefix => @object_name, :field_name => "#{method}(#{position[input]}i)")
649
- item_label_text = I18n.t(input.to_s, :default => input.to_s, :scope => [:formtastic]).send(@@label_str_method)
650
- list_items_capture << template.content_tag(:li,
656
+ opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name)
657
+ item_label_text = I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
658
+
659
+ template.content_tag(:li,
651
660
  template.content_tag(:label, item_label_text, :for => html_id) +
652
- template.send("select_#{input}".intern, @object.send(method), opts)
661
+ template.send("select_#{input}".intern, datetime, opts, html_options.merge(:id => html_id))
653
662
  )
654
663
  end
655
664
  end
656
665
 
657
- template.content_tag(:fieldset,
658
- %{<legend><span>#{label_text(method, options)}</span></legend>} +
659
- template.content_tag(:ol, list_items_capture)
660
- )
666
+ field_set_and_list_wrapping_for_method(method, options, list_items_capture)
661
667
  end
662
668
 
663
- # Outputs a label containing a checkbox and the label text. The label defaults to the column
664
- # name (method name) and can be altered with the :label option.
669
+ # Outputs a label containing a checkbox and the label text. The label defaults
670
+ # to the column name (method name) and can be altered with the :label option.
671
+ #
672
+ # Different from other inputs, :required options has no effect here and
673
+ # :checked_value and :unchecked_value options are also available.
674
+ #
665
675
  def boolean_input(method, options)
666
- input_label(method, options,
667
- template.check_box(@object_name, method, set_options(options)) +
668
- label_text(method, options)
669
- )
676
+ html_options = options.delete(:input_html) || {}
677
+
678
+ content = self.check_box(method, set_options(options).merge(html_options),
679
+ options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
680
+
681
+ # required does not make sense in check box
682
+ input_label(method, content + options.delete(:label), :skip_required => true)
670
683
  end
671
684
 
672
- # Outputs a label and select box containing two options for "true" and "false". The visible
673
- # text defaults to "Yes" and "No" respectively, but can be altered with the :true and :false
674
- # options. The label text to the column name (method name), but can be altered with the
675
- # :label option. Example:
676
- #
677
- # f.input :awesome, :as => :boolean_select, :true => "Yeah!", :false => "Nah!", :label => "Make this sucker public?"
685
+ # Generates an input for the given method using the type supplied with :as.
678
686
  #
679
- # Returns something like:
687
+ # If the input is included in INPUT_MAPPINGS, it uses input_simple
688
+ # implementation which maps most of the inputs. All others have specific
689
+ # code and then a proper handler should be called (like radio_input) for
690
+ # :radio types.
680
691
  #
681
- # <li class="boolean_select required" id="post_public_input">
682
- # <label for="post_public">
683
- # Make this sucker public?<abbr title="required">*</abbr>
684
- # </label>
685
- # <select id="post_public" name="post[public]">
686
- # <option value="1">Yeah!</option>
687
- # <option value="0">Nah!</option>
688
- # </select>
689
- # </li>
690
- #
691
- def boolean_select_input(method, options)
692
- options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic]).send(@@label_str_method)
693
- options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic]).send(@@label_str_method)
692
+ def inline_input_for(method, options)
693
+ input_type = options.delete(:as)
694
694
 
695
- choices = [ [options.delete(:true),true], [options.delete(:false),false] ]
696
- input_label(method, options) + template.select(@object_name, method, choices, set_options(options))
695
+ if INPUT_MAPPINGS.key?(input_type)
696
+ input_simple(input_type, method, options)
697
+ else
698
+ send("#{input_type}_input", method, options)
699
+ end
697
700
  end
698
701
 
699
- # Outputs a fieldset containing two radio buttons (with labels) for "true" and "false". The
700
- # visible label text for each option defaults to "Yes" and "No" respectively, but can be
701
- # altered with the :true and :false options. The fieldset legend defaults to the column name
702
- # (method name), but can be altered with the :label option. Example:
703
- #
704
- # f.input :awesome, :as => :boolean_radio, :true => "Yeah!", :false => "Nah!", :label => "Awesome?"
705
- #
706
- # Returns something like:
702
+ # Generates error messages for the given method. Errors can be shown as list
703
+ # or as sentence. If :none is set, no error is shown.
707
704
  #
708
- # <li class="boolean_radio required" id="post_public_input">
709
- # <fieldset><legend><span>make this sucker public?<abbr title="required">*</abbr></span></legend>
710
- # <ol>
711
- # <li class="true">
712
- # <label for="post_public_true">
713
- # <input id="post_public_true" name="post[public]" type="radio" value="true" /> Yeah!
714
- # </label>
715
- # </li>
716
- # <li class="false">
717
- # <label for="post_public_false">
718
- # <input id="post_public_false" name="post[public]" type="radio" checked="checked" /> Nah!
719
- # </label>
720
- # </li>
721
- # </ol>
722
- # </fieldset>
723
- # </li>
724
- #
725
- def boolean_radio_input(method, options)
726
- options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic]).send(@@label_str_method)
727
- options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic]).send(@@label_str_method)
705
+ def inline_errors_for(method, options) #:nodoc:
706
+ return nil unless @object && [:sentence, :list].include?(@@inline_errors)
728
707
 
729
- choices = { options.delete(:true) => true, options.delete(:false) => false }
730
- radio_input(method, { :collection => choices, :value_as_class => true }.merge(options))
708
+ errors = @object.errors.on(method.to_s).to_a
709
+ send("error_#{@@inline_errors}", errors) unless errors.empty?
731
710
  end
732
711
 
733
- def inline_errors(method, options) #:nodoc:
734
- errors = @object.errors.on(method.to_s).to_a
735
- unless errors.empty?
736
- send("error_#{@@inline_errors}", errors) if [:sentence, :list].include?(@@inline_errors)
737
- end
712
+ # Generates hints for the given method using the text supplied in :hint.
713
+ #
714
+ def inline_hints_for(method, options) #:nodoc:
715
+ options[:hint].blank? ? '' : template.content_tag(:p, options[:hint], :class => 'inline-hints')
738
716
  end
739
717
 
718
+ # Creates an error sentence by calling to_sentence on the errors array.
719
+ #
740
720
  def error_sentence(errors) #:nodoc:
741
721
  template.content_tag(:p, errors.to_sentence, :class => 'inline-errors')
742
722
  end
743
723
 
724
+ # Creates an error li list.
725
+ #
744
726
  def error_list(errors) #:nodoc:
745
727
  list_elements = []
746
728
  errors.each do |error|
@@ -749,19 +731,24 @@ module Formtastic #:nodoc:
749
731
  template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
750
732
  end
751
733
 
752
- def inline_hints(method, options) #:nodoc:
753
- options[:hint].blank? ? '' : template.content_tag(:p, options[:hint], :class => 'inline-hints')
754
- end
755
-
756
- def label_text(method, options) #:nodoc:
757
- [ options[:label], required_or_optional_string(options[:required]) ].join()
758
- end
734
+ # Generates the label for the input. Accepts the same options as Rails label
735
+ # method and a fourth option that allows the label to be generated as span
736
+ # with class label.
737
+ #
738
+ def input_label(method, text, options={}, as_span=false) #:nodoc:
739
+ text << required_or_optional_string(options.delete(:required)) unless options.delete(:skip_required)
759
740
 
760
- def input_label(method, options, text = nil) #:nodoc:
761
- text ||= label_text(method, options)
762
- template.label(@object_name, method, text, set_options(options))
741
+ if as_span
742
+ options[:class] ||= 'label'
743
+ template.content_tag(:span, text, options)
744
+ else
745
+ self.label(method, text, options)
746
+ end
763
747
  end
764
748
 
749
+ # Generates the required or optional string. If the value set is a proc,
750
+ # it evaluates the proc first.
751
+ #
765
752
  def required_or_optional_string(required) #:nodoc:
766
753
  string_or_proc = required ? @@required_string : @@optional_string
767
754
 
@@ -772,24 +759,46 @@ module Formtastic #:nodoc:
772
759
  end
773
760
  end
774
761
 
775
- def field_set_and_list_wrapping(field_set_html_options, contents = '', &block) #:nodoc:
776
- legend_text = field_set_html_options.delete(:name)
762
+ # Generates a fieldset and wraps the content in an ordered list. When working
763
+ # with nested attributes (in Rails 2.3), it allows %i as interpolation option
764
+ # in :name. So you can do:
765
+ #
766
+ # f.inputs :name => 'Task #%i', :for => :tasks
767
+ #
768
+ # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
769
+ # 'Task #3' and so on.
770
+ #
771
+ # If you are using inputs :for, for more than one association in the same
772
+ # form builder, you might want to set the nested_child_index as well. You
773
+ # can do that doing:
774
+ #
775
+ # f.nested_child_index = -1
776
+ #
777
+ def field_set_and_list_wrapping(html_options, contents = '', &block) #:nodoc:
778
+ # Generates the legend text allowing nested_child_index support for interpolation
779
+ legend_text = html_options.delete(:name).to_s
780
+ legend_text %= html_options[:parent_builder].instance_variable_get('@nested_child_index').to_i + 1
781
+
777
782
  legend = legend_text.blank? ? "" : template.content_tag(:legend, template.content_tag(:span, legend_text))
778
- if block_given?
779
- contents = template.capture(&block)
780
- template.concat(
781
- template.content_tag(:fieldset,
782
- legend + template.content_tag(:ol, contents),
783
- field_set_html_options
784
- )
785
- )
786
- else
787
- template.content_tag(:fieldset,
788
- legend + template.content_tag(:ol, contents),
789
- field_set_html_options
790
- )
791
- end
783
+ contents = template.capture(&block) if block_given?
784
+
785
+ fieldset = template.content_tag(:fieldset,
786
+ legend + template.content_tag(:ol, contents),
787
+ html_options.except(:builder, :parent_builder)
788
+ )
792
789
 
790
+ template.concat(fieldset) if block_given?
791
+ fieldset
792
+ end
793
+
794
+ # Also generates a fieldset and an ordered list but with label based in
795
+ # method. This methods is currently used by radio and datetime inputs.
796
+ #
797
+ def field_set_and_list_wrapping_for_method(method, options, contents)
798
+ template.content_tag(:fieldset,
799
+ %{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} +
800
+ template.content_tag(:ol, contents)
801
+ )
793
802
  end
794
803
 
795
804
  # For methods that have a database column, take a best guess as to what the inout method
@@ -799,46 +808,99 @@ module Formtastic #:nodoc:
799
808
  #
800
809
  # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
801
810
  # default is a :string, a similar behaviour to Rails' scaffolding.
802
- def default_input_type(object, method) #:nodoc:
811
+ #
812
+ def default_input_type(method) #:nodoc:
813
+ return :string if @object.nil?
814
+
803
815
  # Find the column object by attribute
804
- column = object.column_for_attribute(method) if object.respond_to?(:column_for_attribute)
805
- # Maybe the column is a reflection?
806
- column = find_reflection(method) unless column
816
+ column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
817
+
818
+ # Associations map by default to a select
819
+ return :select if column.nil? && find_reflection(method)
807
820
 
808
821
  if column
809
822
  # handle the special cases where the column type doesn't map to an input method
810
- return :select if column.respond_to?(:macro) && column.respond_to?(:klass)
811
- return :select if column.type == :integer && method.to_s =~ /_id$/
823
+ return :select if column.type == :integer && method.to_s =~ /_id$/
812
824
  return :datetime if column.type == :timestamp
813
- return :numeric if [:integer, :float, :decimal].include?(column.type)
825
+ return :numeric if [:integer, :float, :decimal].include?(column.type)
814
826
  return :password if column.type == :string && method.to_s =~ /password/
815
827
  # otherwise assume the input name will be the same as the column type (eg string_input)
816
828
  return column.type
817
829
  else
818
- obj = object.send(method)
819
- return :file if [:file?, :public_filename].any? { |m| obj.respond_to?(m) }
830
+ obj = @object.send(method) if @object.respond_to?(method)
831
+
832
+ return :file if obj && @@file_methods.any? { |m| obj.respond_to?(m) }
820
833
  return :password if method.to_s =~ /password/
821
834
  return :string
822
835
  end
823
836
  end
824
837
 
825
- # Used by association inputs (select, radio) to get a default collection from the parent object
826
- # by determining the classname from the method/column name (section_id => Section) and doing a
827
- # simple find(:all).
828
- def find_parent_objects_for_column(column)
829
- parent_class = if reflection = find_reflection(column)
830
- reflection.klass
838
+ # Used by select and radio inputs. The collection can be retrieved by
839
+ # three ways:
840
+ #
841
+ # * Explicitly provided through :collection
842
+ # * Retrivied through an association
843
+ # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
844
+ #
845
+ # If the collection is not a hash or an array of strings, fixnums or arrays,
846
+ # we use label_method and value_method to retreive an array with the
847
+ # appropriate label and value.
848
+ #
849
+ def find_collection_for_column(column, options)
850
+ reflection = find_reflection(column)
851
+
852
+ collection = if options[:collection]
853
+ options.delete(:collection)
854
+ elsif reflection || column.to_s =~ /_id$/
855
+ parent_class = if reflection
856
+ reflection.klass
857
+ else
858
+ ::ActiveSupport::Deprecation.warn("The _id way of doing things is deprecated. Please use the association method (#{column.to_s.sub(/_id$/,'')})", caller[3..-1])
859
+ column.to_s.sub(/_id$/,'').camelize.constantize
860
+ end
861
+
862
+ parent_class.find(:all)
831
863
  else
832
- ::ActiveSupport::Deprecation.warn("The _id way of doing things is deprecated. Please use the association method (#{column.to_s.sub(/_id$/,'')})", caller[3..-1])
833
- column.to_s.sub(/_id$/,'').camelize.constantize
864
+ create_boolean_collection(options)
834
865
  end
835
- parent_class.find(:all)
866
+
867
+ collection = collection.to_a if collection.instance_of?(Hash)
868
+
869
+ # Return if we have an Array of strings, fixnums or arrays
870
+ return collection if collection.instance_of?(Array) &&
871
+ [Array, Fixnum, String, Symbol].include?(collection.first.class)
872
+
873
+ label = options.delete(:label_method) || detect_label_method(collection)
874
+ value = options.delete(:value_method) || :id
875
+
876
+ collection.map { |o| [o.send(label), o.send(value)] }
836
877
  end
837
878
 
838
- # Used by association inputs (select, radio) to generate the name that should be used for the input
839
- # belongs_to :author; f.input :author; will generate 'author_id'
840
- # has_many :authors; f.input :authors; will generate 'author_ids'
841
- # has_and_belongs_to_many will act like has_many
879
+ # Detected the label collection method when none is supplied using the
880
+ # values set in @@collection_label_methods.
881
+ #
882
+ def detect_label_method(collection) #:nodoc:
883
+ @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
884
+ end
885
+
886
+ # Returns a hash to be used by radio and select inputs when a boolean field
887
+ # is provided.
888
+ #
889
+ def create_boolean_collection(options)
890
+ options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic])
891
+ options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic])
892
+ options[:value_as_class] = true unless options.key?(:value_as_class)
893
+
894
+ { options.delete(:true) => true, options.delete(:false) => false }
895
+ end
896
+
897
+ # Used by association inputs (select, radio) to generate the name that should
898
+ # be used for the input
899
+ #
900
+ # belongs_to :author; f.input :author; will generate 'author_id'
901
+ # has_many :authors; f.input :authors; will generate 'author_ids'
902
+ # has_and_belongs_to_many will act like has_many
903
+ #
842
904
  def generate_association_input_name(method)
843
905
  if reflection = find_reflection(method)
844
906
  method = "#{method.to_s.singularize}_id"
@@ -847,25 +909,24 @@ module Formtastic #:nodoc:
847
909
  method
848
910
  end
849
911
 
850
- # If an association method is passed in (f.input :author) try to find the reflection object
912
+ # If an association method is passed in (f.input :author) try to find the
913
+ # reflection object.
914
+ #
851
915
  def find_reflection(method)
852
916
  object.class.reflect_on_association(method) if object.class.respond_to?(:reflect_on_association)
853
917
  end
854
918
 
919
+ # Generates default_string_options by retrieving column information from
920
+ # the database.
921
+ #
855
922
  def default_string_options(method) #:nodoc:
856
- # Use rescue to set column if @object does not have a column_for_attribute method
857
- # (eg if @object is not an ActiveRecord object)
858
- begin
859
- column = @object.column_for_attribute(method)
860
- rescue NoMethodError
861
- column = nil
862
- end
863
- opts = if column.nil? || column.limit.nil?
923
+ column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
924
+
925
+ if column.nil? || column.limit.nil?
864
926
  { :size => @@default_text_field_size }
865
927
  else
866
928
  { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
867
929
  end
868
- set_options(opts)
869
930
  end
870
931
 
871
932
  # Generate the html id for the li tag.
@@ -931,6 +992,7 @@ module Formtastic #:nodoc:
931
992
  # Please note: Although it's possible to call Rails' built-in form_for() helper without an
932
993
  # object, all semantic forms *must* have an object (either Post.new or @post), as Formtastic
933
994
  # has too many dependencies on an ActiveRecord object being present.
995
+ #
934
996
  module SemanticFormHelper
935
997
  @@builder = Formtastic::SemanticFormBuilder
936
998