formtastic 0.9.4 → 0.9.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -446,7 +446,22 @@ $ ./script/generate form Post --partial
446
446
  create app/views/posts/_form.html.erb
447
447
  </pre>
448
448
 
449
- To generate *HAML* markup, just add the @--haml@ as argument: @./script/generate form Post --haml@
449
+ To generate *HAML* markup, just add the @--haml@ as argument:
450
+
451
+ <pre>
452
+ $ ./script/generate form Post --haml
453
+ exists app/views/admin/posts
454
+ create app/views/admin/posts/_form.html.haml
455
+ </pre>
456
+
457
+ To specify the controller in a namespace (eg admin/posts instead of posts), use the --controller argument:
458
+
459
+ <pre>
460
+ $ ./script/generate form Post --partial --controller admin/posts
461
+ exists app/views/admin/posts
462
+ create app/views/admin/posts/_form.html.erb
463
+ </pre>
464
+
450
465
 
451
466
  h2. Custom Inputs
452
467
 
@@ -455,6 +470,8 @@ If you want to add your own input types to encapsulate your own logic or interfa
455
470
  @Formtastic::SemanticFormHelper.builder = MyCustomBuilder@
456
471
 
457
472
 
473
+
474
+
458
475
  h2. Status
459
476
 
460
477
  Formtastic has been in active development for about a year. We've just recently jumped to an 0.9 version number, signaling that we consider this a 1.0 release candidate, and that the API won't change significantly for the 1.x series.
@@ -464,7 +481,7 @@ h2. Dependencies
464
481
 
465
482
  There are none, but...
466
483
 
467
- * if you have the "ValidationReflection":http://github.com/redinger/validation_reflection plugin is installed, you won't have to specify the :required option (it checks the validations on the model instead).
484
+ * if you have the "ValidationReflection":http://github.com/redinger/validation_reflection plugin is installed, you won't have to specify the @:required@ option (it checks the validations on the model instead).
468
485
  * if you want to use the @:country@ input, you'll need to install the "iso-3166-country-select plugin":http://github.com/rails/iso-3166-country-select (or any other country_select plugin with the same API).
469
486
  * "rspec":http://github.com/dchelimsky/rspec/, "rspec_hpricot_matchers":http://rubyforge.org/projects/rspec-hpricot/ and "rcov":http://github.com/relevance/rcov gems (plus any of their own dependencies) are required for the test suite.
470
487
 
data/Rakefile CHANGED
@@ -23,7 +23,7 @@ begin
23
23
  ========================================================================
24
24
  Thanks for installing Formtastic!
25
25
  ------------------------------------------------------------------------
26
- You can now (optionally) run the generater to copy some stylesheets and
26
+ You can now (optionally) run the generator to copy some stylesheets and
27
27
  a config initializer into your application:
28
28
  ./script/generate formtastic
29
29
 
@@ -59,13 +59,12 @@ begin
59
59
  # Development dependencies. Not installed by default.
60
60
  # Install with: sudo gem install formtastic --development
61
61
  s.add_development_dependency 'rspec-rails', '>= 1.2.6'
62
- s.add_development_dependency 'hpricot', '>= 0.6.1' # for: rspec_hpricot_matchers
63
- s.add_development_dependency 'rspec_hpricot_matchers', '>= 1.0.0'
62
+ s.add_development_dependency 'rspec_tag_matchers', '>= 1.0.0'
64
63
  end
65
64
 
66
65
  Jeweler::GemcutterTasks.new
67
66
  rescue LoadError
68
- puts "[formtastic:] Jeweler - or one of it's dependencies - is not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
67
+ puts "[formtastic:] Jeweler - or one of its dependencies - is not available. Install it with: sudo gem install jeweler -s http://gemcutter.org"
69
68
  end
70
69
 
71
70
  desc 'Default: run unit specs.'
@@ -39,10 +39,11 @@ class FormGenerator < Rails::Generator::NamedBase
39
39
  def manifest
40
40
  record do |m|
41
41
  if options[:partial]
42
+ controller_and_view_path = options[:controller] || File.join(controller_class_path, controller_file_name)
42
43
  # Ensure directory exists.
43
- m.directory File.join(VIEWS_PATH, controller_class_path, controller_file_name)
44
+ m.directory File.join(VIEWS_PATH, controller_and_view_path)
44
45
  # Create a form partial for the model as "_form" in it's views path.
45
- m.template "view__form.html.#{template_type}", File.join(VIEWS_PATH, controller_file_name, "_form.html.#{template_type}")
46
+ m.template "view__form.html.#{template_type}", File.join(VIEWS_PATH, controller_and_view_path, "_form.html.#{template_type}")
46
47
  else
47
48
  # Load template file, and render without saving to file
48
49
  template = File.read(File.join(source_root, "view__form.html.#{template_type}"))
@@ -85,7 +86,7 @@ class FormGenerator < Rails::Generator::NamedBase
85
86
 
86
87
  # Add additional model attributes if specified in args - probably not that common scenario.
87
88
  def attributes
88
- # Get columns for the requested model
89
+ # Get columns for the requested model.
89
90
  existing_attributes = @class_name.constantize.content_columns.reject { |column| IGNORED_COLUMNS.include?(column.name.to_sym) }
90
91
  @args = super + existing_attributes
91
92
  end
@@ -105,6 +106,11 @@ class FormGenerator < Rails::Generator::NamedBase
105
106
  "Save generated output directly to a form partial (app/views/{resource}/_form.html.*).") do |v|
106
107
  options[:partial] = v
107
108
  end
109
+
110
+ opt.on('--controller CONTROLLER_PATH',
111
+ "Specify a non-standard controller for the specified model (e.g. admin/posts).") do |v|
112
+ options[:controller] = v if v.present?
113
+ end
108
114
  end
109
115
 
110
116
  def banner
@@ -27,10 +27,6 @@ module Formtastic #:nodoc:
27
27
 
28
28
  INLINE_ERROR_TYPES = [:sentence, :list, :first]
29
29
 
30
- I18N_SCOPES = [ '{{model}}.{{action}}.{{attribute}}',
31
- '{{model}}.{{attribute}}',
32
- '{{attribute}}']
33
-
34
30
  attr_accessor :template
35
31
 
36
32
  # Returns a suitable form input for the given +method+, using the database column information
@@ -80,7 +76,7 @@ module Formtastic #:nodoc:
80
76
  #
81
77
  def input(method, options = {})
82
78
  options[:required] = method_required?(method) unless options.key?(:required)
83
- options[:as] ||= default_input_type(method)
79
+ options[:as] ||= default_input_type(method, options)
84
80
 
85
81
  html_class = [ options[:as], (options[:required] ? :required : :optional) ]
86
82
  html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
@@ -96,7 +92,7 @@ module Formtastic #:nodoc:
96
92
 
97
93
  input_parts = @@inline_order.dup
98
94
  input_parts.delete(:errors) if options[:as] == :hidden
99
-
95
+
100
96
  list_item_content = input_parts.map do |type|
101
97
  send(:"inline_#{type}_for", method, options)
102
98
  end.compact.join("\n")
@@ -251,11 +247,13 @@ module Formtastic #:nodoc:
251
247
  # instead (just as you would do with Rails' form builder).
252
248
  #
253
249
  def inputs(*args, &block)
250
+ title = field_set_title_from_args(*args)
254
251
  html_options = args.extract_options!
255
252
  html_options[:class] ||= "inputs"
256
-
257
- if html_options[:for]
258
- inputs_for_nested_attributes(args, html_options, &block)
253
+ html_options[:name] = title
254
+
255
+ if html_options[:for] # Nested form
256
+ inputs_for_nested_attributes(*(args << html_options), &block)
259
257
  elsif block_given?
260
258
  field_set_and_list_wrapping(*(args << html_options), &block)
261
259
  else
@@ -319,7 +317,6 @@ module Formtastic #:nodoc:
319
317
  key = :submit
320
318
  object_name = @object_name.to_s.send(@@label_str_method)
321
319
  end
322
- fallback_text ||= "#{key.to_s.humanize} {{model}}"
323
320
 
324
321
  text = (self.localized_string(key, text, :action, :model => object_name) ||
325
322
  ::Formtastic::I18n.t(key, :model => object_name)) unless text.is_a?(::String)
@@ -353,7 +350,7 @@ module Formtastic #:nodoc:
353
350
  #
354
351
  def semantic_fields_for(record_or_name_or_array, *args, &block)
355
352
  opts = args.extract_options!
356
- opts.merge!(:builder => ::Formtastic::SemanticFormHelper.builder)
353
+ opts[:builder] ||= Formtastic::SemanticFormHelper.builder
357
354
  args.push(opts)
358
355
  fields_for(record_or_name_or_array, *args, &block)
359
356
  end
@@ -407,1018 +404,1150 @@ module Formtastic #:nodoc:
407
404
  # f.errors_on(:body)
408
405
  # end
409
406
  #
410
- def inline_errors_for(method, options=nil) #:nodoc:
411
- return nil unless @object && @object.respond_to?(:errors) && INLINE_ERROR_TYPES.include?(@@inline_errors)
412
-
413
- errors = @object.errors[method.to_sym]
414
- send(:"error_#{@@inline_errors}", Array(errors)) unless errors.blank?
407
+ def inline_errors_for(method, options = nil) #:nodoc:
408
+ if render_inline_errors?
409
+ errors = @object.errors[method.to_sym]
410
+ send(:"error_#{@@inline_errors}", [*errors]) if errors.present?
411
+ else
412
+ nil
413
+ end
415
414
  end
416
415
  alias :errors_on :inline_errors_for
417
416
 
418
417
  protected
419
418
 
420
- # Collects content columns (non-relation columns) for the current form object class.
421
- #
422
- def content_columns
423
- if @object.present?
424
- @object.class.name.constantize.content_columns.collect { |c| c.name.to_sym }.compact
425
- else
426
- @object_name.to_s.classify.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue []
419
+ def render_inline_errors?
420
+ @object && @object.respond_to?(:errors) && INLINE_ERROR_TYPES.include?(@@inline_errors)
427
421
  end
428
- end
429
422
 
430
- # Collects association columns (relation columns) for the current form object class.
431
- #
432
- def association_columns(*by_associations)
433
- if @object.present?
434
- @object.class.reflections.collect do |name, _|
435
- if by_associations.present?
436
- name if by_associations.include?(_.macro)
437
- else
438
- name
439
- end
440
- end.compact
441
- else
442
- []
423
+ # Collects content columns (non-relation columns) for the current form object class.
424
+ #
425
+ def content_columns #:nodoc:
426
+ self.model_name.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue []
443
427
  end
444
- end
445
428
 
446
- # Prepare options to be sent to label
447
- #
448
- def options_for_label(options)
449
- options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
450
- end
429
+ # Collects association columns (relation columns) for the current form object class.
430
+ #
431
+ def association_columns(*by_associations) #:nodoc:
432
+ if @object.present?
433
+ @object.class.reflections.collect do |name, _|
434
+ if by_associations.present?
435
+ name if by_associations.include?(_.macro)
436
+ else
437
+ name
438
+ end
439
+ end.compact
440
+ else
441
+ []
442
+ end
443
+ end
451
444
 
452
- # Deals with :for option when it's supplied to inputs methods. Additional
453
- # options to be passed down to :for should be supplied using :for_options
454
- # key.
455
- #
456
- # It should raise an error if a block with arity zero is given.
457
- #
458
- def inputs_for_nested_attributes(args, options, &block)
459
- args << options.merge!(:parent => { :builder => self, :for => options[:for] })
445
+ # Prepare options to be sent to label
446
+ #
447
+ def options_for_label(options) #:nodoc:
448
+ options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
449
+ end
460
450
 
461
- fields_for_block = if block_given?
462
- raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
463
- 'but the block does not accept any argument.' if block.arity <= 0
451
+ # Deals with :for option when it's supplied to inputs methods. Additional
452
+ # options to be passed down to :for should be supplied using :for_options
453
+ # key.
454
+ #
455
+ # It should raise an error if a block with arity zero is given.
456
+ #
457
+ def inputs_for_nested_attributes(*args, &block) #:nodoc:
458
+ options = args.extract_options!
459
+ args << options.merge!(:parent => { :builder => self, :for => options[:for] })
460
+
461
+ fields_for_block = if block_given?
462
+ raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
463
+ 'but the block does not accept any argument.' if block.arity <= 0
464
+
465
+ proc { |f| f.inputs(*args){ block.call(f) } }
466
+ else
467
+ proc { |f| f.inputs(*args) }
468
+ end
464
469
 
465
- proc { |f| f.inputs(*args){ block.call(f) } }
466
- else
467
- proc { |f| f.inputs(*args) }
470
+ fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
471
+ semantic_fields_for(*fields_for_args, &fields_for_block)
468
472
  end
469
473
 
470
- fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
471
- semantic_fields_for(*fields_for_args, &fields_for_block)
472
- end
473
-
474
- # Remove any Formtastic-specific options before passing the down options.
475
- #
476
- def set_options(options)
477
- options.except(:value_method, :label_method, :collection, :required, :label,
478
- :as, :hint, :input_html, :label_html, :value_as_class)
479
- end
474
+ # Remove any Formtastic-specific options before passing the down options.
475
+ #
476
+ def strip_formtastic_options(options) #:nodoc:
477
+ options.except(:value_method, :label_method, :collection, :required, :label,
478
+ :as, :hint, :input_html, :label_html, :value_as_class)
479
+ end
480
480
 
481
- # Determins if the attribute (eg :title) should be considered required or not.
482
- #
483
- # * if the :required option was provided in the options hash, the true/false value will be
484
- # returned immediately, allowing the view to override any guesswork that follows:
485
- #
486
- # * if the :required option isn't provided in the options hash, and the ValidationReflection
487
- # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
488
- # if the validates_presence_of macro has been used in the class for this attribute, or false
489
- # otherwise.
490
- #
491
- # * if the :required option isn't provided, and the plugin isn't available, the value of the
492
- # configuration option @@all_fields_required_by_default is used.
493
- #
494
- def method_required?(attribute) #:nodoc:
495
- if @object && @object.class.respond_to?(:reflect_on_validations_for)
496
- attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
497
-
498
- @object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
499
- validation.macro == :validates_presence_of &&
500
- validation.name == attribute_sym &&
501
- (validation.options.present? ? options_require_validation?(validation.options) : true)
481
+ # Determins if the attribute (eg :title) should be considered required or not.
482
+ #
483
+ # * if the :required option was provided in the options hash, the true/false value will be
484
+ # returned immediately, allowing the view to override any guesswork that follows:
485
+ #
486
+ # * if the :required option isn't provided in the options hash, and the ValidationReflection
487
+ # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
488
+ # if the validates_presence_of macro has been used in the class for this attribute, or false
489
+ # otherwise.
490
+ #
491
+ # * if the :required option isn't provided, and the plugin isn't available, the value of the
492
+ # configuration option @@all_fields_required_by_default is used.
493
+ #
494
+ def method_required?(attribute) #:nodoc:
495
+ if @object && @object.class.respond_to?(:reflect_on_validations_for)
496
+ attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
497
+
498
+ @object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
499
+ validation.macro == :validates_presence_of &&
500
+ validation.name == attribute_sym &&
501
+ (validation.options.present? ? options_require_validation?(validation.options) : true)
502
+ end
503
+ else
504
+ @@all_fields_required_by_default
502
505
  end
503
- else
504
- @@all_fields_required_by_default
505
506
  end
506
- end
507
507
 
508
- # Determines whether the given options evaluate to true
509
- def options_require_validation?(options) #nodoc
510
- if_condition = !options[:if].nil?
511
- condition = if_condition ? options[:if] : options[:unless]
508
+ # Determines whether the given options evaluate to true
509
+ def options_require_validation?(options) #nodoc
510
+ if_condition = !options[:if].nil?
511
+ condition = if_condition ? options[:if] : options[:unless]
512
512
 
513
- condition = if condition.respond_to?(:call)
514
- condition.call(@object)
515
- elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
516
- @object.send(condition)
517
- else
518
- condition
519
- end
513
+ condition = if condition.respond_to?(:call)
514
+ condition.call(@object)
515
+ elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
516
+ @object.send(condition)
517
+ else
518
+ condition
519
+ end
520
520
 
521
- if_condition ? !!condition : !condition
522
- end
521
+ if_condition ? !!condition : !condition
522
+ end
523
523
 
524
- def basic_input_helper(form_helper_method, type, method, options)
525
- html_options = options.delete(:input_html) || {}
526
- html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password].include?(type)
524
+ def basic_input_helper(form_helper_method, type, method, options) #:nodoc:
525
+ html_options = options.delete(:input_html) || {}
526
+ html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password].include?(type)
527
527
 
528
- self.label(method, options_for_label(options)) <<
529
- self.send(form_helper_method, method, html_options)
530
- end
531
-
532
- # Outputs a label and standard Rails text field inside the wrapper.
533
- def string_input(method, options)
534
- basic_input_helper(:text_field, :string, method, options)
535
- end
528
+ self.label(method, options_for_label(options)) <<
529
+ self.send(form_helper_method, method, html_options)
530
+ end
536
531
 
537
- # Outputs a label and standard Rails password field inside the wrapper.
538
- def password_input(method, options)
539
- basic_input_helper(:password_field, :password, method, options)
540
- end
532
+ # Outputs a label and standard Rails text field inside the wrapper.
533
+ def string_input(method, options)
534
+ basic_input_helper(:text_field, :string, method, options)
535
+ end
541
536
 
542
- # Outputs a label and standard Rails text field inside the wrapper.
543
- def numeric_input(method, options)
544
- basic_input_helper(:text_field, :numeric, method, options)
545
- end
537
+ # Outputs a label and standard Rails password field inside the wrapper.
538
+ def password_input(method, options)
539
+ basic_input_helper(:password_field, :password, method, options)
540
+ end
546
541
 
547
- # Ouputs a label and standard Rails text area inside the wrapper.
548
- def text_input(method, options)
549
- basic_input_helper(:text_area, :text, method, options)
550
- end
542
+ # Outputs a label and standard Rails text field inside the wrapper.
543
+ def numeric_input(method, options)
544
+ basic_input_helper(:text_field, :numeric, method, options)
545
+ end
551
546
 
552
- # Outputs a label and a standard Rails file field inside the wrapper.
553
- def file_input(method, options)
554
- basic_input_helper(:file_field, :file, method, options)
555
- end
547
+ # Ouputs a label and standard Rails text area inside the wrapper.
548
+ def text_input(method, options)
549
+ basic_input_helper(:text_area, :text, method, options)
550
+ end
556
551
 
557
- # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
558
- # Additionals options can be given and will be sent straight to hidden input
559
- # element.
560
- #
561
- def hidden_input(method, options)
562
- options ||= {}
563
- if options[:input_html].present?
564
- options[:value] = options[:input_html][:value] if options[:input_html][:value].present?
552
+ # Outputs a label and a standard Rails file field inside the wrapper.
553
+ def file_input(method, options)
554
+ basic_input_helper(:file_field, :file, method, options)
565
555
  end
566
- self.hidden_field(method, set_options(options))
567
- end
568
556
 
569
- # Outputs a label and a select box containing options from the parent
570
- # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
571
- # is has_many or has_and_belongs_to_many the select box will be set as multi-select
572
- # and size = 5
573
- #
574
- # Example (belongs_to):
575
- #
576
- # f.input :author
577
- #
578
- # <label for="book_author_id">Author</label>
579
- # <select id="book_author_id" name="book[author_id]">
580
- # <option value=""></option>
581
- # <option value="1">Justin French</option>
582
- # <option value="2">Jane Doe</option>
583
- # </select>
584
- #
585
- # Example (has_many):
586
- #
587
- # f.input :chapters
588
- #
589
- # <label for="book_chapter_ids">Chapters</label>
590
- # <select id="book_chapter_ids" name="book[chapter_ids]">
591
- # <option value=""></option>
592
- # <option value="1">Chapter 1</option>
593
- # <option value="2">Chapter 2</option>
594
- # </select>
595
- #
596
- # Example (has_and_belongs_to_many):
597
- #
598
- # f.input :authors
599
- #
600
- # <label for="book_author_ids">Authors</label>
601
- # <select id="book_author_ids" name="book[author_ids]">
602
- # <option value=""></option>
603
- # <option value="1">Justin French</option>
604
- # <option value="2">Jane Doe</option>
605
- # </select>
606
- #
607
- #
608
- # You can customize the options available in the select by passing in a collection (an Array or
609
- # Hash) through the :collection option. If not provided, the choices are found by inferring the
610
- # parent's class name from the method name and simply calling find(:all) on it
611
- # (VehicleOwner.find(:all) in the example above).
612
- #
613
- # Examples:
614
- #
615
- # f.input :author, :collection => @authors
616
- # f.input :author, :collection => Author.find(:all)
617
- # f.input :author, :collection => [@justin, @kate]
618
- # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
619
- # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
620
- #
621
- # The :label_method option allows you to customize the text label inside each option tag two ways:
622
- #
623
- # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
624
- # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
625
- #
626
- # Examples:
627
- #
628
- # f.input :author, :label_method => :full_name
629
- # f.input :author, :label_method => :login
630
- # f.input :author, :label_method => :full_name_with_post_count
631
- # f.input :author, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
632
- #
633
- # The :value_method option provides the same customization of the value attribute of each option tag.
634
- #
635
- # Examples:
636
- #
637
- # f.input :author, :value_method => :full_name
638
- # f.input :author, :value_method => :login
639
- # f.input :author, :value_method => Proc.new { |a| "author_#{a.login}" }
640
- #
641
- # You can pre-select a specific option value by passing in the :selected option.
642
- #
643
- # Examples:
644
- #
645
- # f.input :author, :selected => current_user.id
646
- # f.input :author, :value_method => :login, :selected => current_user.login
647
- #
648
- # You can pass html_options to the select tag using :input_html => {}
649
- #
650
- # Examples:
651
- #
652
- # f.input :authors, :input_html => {:size => 20, :multiple => true}
653
- #
654
- # By default, all select inputs will have a blank option at the top of the list. You can add
655
- # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
656
- #
657
- #
658
- # You can group the options in optgroup elements by passing the :group_by option
659
- # (Note: only tested for belongs_to relations)
660
- #
661
- # Examples:
662
- #
663
- # f.input :author, :group_by => :continent
664
- #
665
- # All the other options should work as expected. If you want to call a custom method on the
666
- # group item. You can include the option:group_label_method
667
- # Examples:
668
- #
669
- # f.input :author, :group_by => :continents, :group_label_method => :something_different
670
- #
671
- def select_input(method, options)
672
- html_options = options.delete(:input_html) || {}
673
- options = set_include_blank(options)
674
-
675
- reflection = find_reflection(method)
676
- if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
677
- options[:include_blank] = false
678
- html_options[:multiple] ||= true
679
- html_options[:size] ||= 5
557
+ # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
558
+ # Additionals options can be given and will be sent straight to hidden input
559
+ # element.
560
+ #
561
+ def hidden_input(method, options)
562
+ options ||= {}
563
+ if options[:input_html].present?
564
+ options[:value] = options[:input_html][:value] if options[:input_html][:value].present?
565
+ end
566
+ self.hidden_field(method, strip_formtastic_options(options))
680
567
  end
681
- input_name = generate_association_input_name(method)
682
-
683
- select_html = if options[:group_by]
684
- # The grouped_options_select is a bit counter intuitive and not optimised (mostly due to ActiveRecord).
685
- # The formtastic user however shouldn't notice this too much.
686
- raw_collection = find_raw_collection_for_column(method, options.reverse_merge(:find_options => { :include => options[:group_by] }))
687
- label, value = detect_label_and_value_method!(raw_collection)
688
- group_collection = raw_collection.map { |option| option.send(options[:group_by]) }.uniq
689
- group_label_method = options[:group_label_method] || detect_label_method(group_collection)
690
- group_collection = group_collection.sort_by { |group_item| group_item.send(group_label_method) }
691
-
692
- # Here comes the monster with 8 arguments
693
- self.grouped_collection_select(input_name, group_collection,
694
- method.to_s.pluralize, group_label_method,
695
- value, label,
696
- set_options(options), html_options)
697
- else
698
- collection = find_collection_for_column(method, options)
699
- self.select(input_name, collection, set_options(options), html_options)
568
+
569
+ # Outputs a label and a select box containing options from the parent
570
+ # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
571
+ # is has_many or has_and_belongs_to_many the select box will be set as multi-select
572
+ # and size = 5
573
+ #
574
+ # Example (belongs_to):
575
+ #
576
+ # f.input :author
577
+ #
578
+ # <label for="book_author_id">Author</label>
579
+ # <select id="book_author_id" name="book[author_id]">
580
+ # <option value=""></option>
581
+ # <option value="1">Justin French</option>
582
+ # <option value="2">Jane Doe</option>
583
+ # </select>
584
+ #
585
+ # Example (has_many):
586
+ #
587
+ # f.input :chapters
588
+ #
589
+ # <label for="book_chapter_ids">Chapters</label>
590
+ # <select id="book_chapter_ids" name="book[chapter_ids]">
591
+ # <option value=""></option>
592
+ # <option value="1">Chapter 1</option>
593
+ # <option value="2">Chapter 2</option>
594
+ # </select>
595
+ #
596
+ # Example (has_and_belongs_to_many):
597
+ #
598
+ # f.input :authors
599
+ #
600
+ # <label for="book_author_ids">Authors</label>
601
+ # <select id="book_author_ids" name="book[author_ids]">
602
+ # <option value=""></option>
603
+ # <option value="1">Justin French</option>
604
+ # <option value="2">Jane Doe</option>
605
+ # </select>
606
+ #
607
+ #
608
+ # You can customize the options available in the select by passing in a collection (an Array or
609
+ # Hash) through the :collection option. If not provided, the choices are found by inferring the
610
+ # parent's class name from the method name and simply calling find(:all) on it
611
+ # (VehicleOwner.find(:all) in the example above).
612
+ #
613
+ # Examples:
614
+ #
615
+ # f.input :author, :collection => @authors
616
+ # f.input :author, :collection => Author.find(:all)
617
+ # f.input :author, :collection => [@justin, @kate]
618
+ # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
619
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
620
+ #
621
+ # The :label_method option allows you to customize the text label inside each option tag two ways:
622
+ #
623
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
624
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
625
+ #
626
+ # Examples:
627
+ #
628
+ # f.input :author, :label_method => :full_name
629
+ # f.input :author, :label_method => :login
630
+ # f.input :author, :label_method => :full_name_with_post_count
631
+ # f.input :author, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
632
+ #
633
+ # The :value_method option provides the same customization of the value attribute of each option tag.
634
+ #
635
+ # Examples:
636
+ #
637
+ # f.input :author, :value_method => :full_name
638
+ # f.input :author, :value_method => :login
639
+ # f.input :author, :value_method => Proc.new { |a| "author_#{a.login}" }
640
+ #
641
+ # You can pre-select a specific option value by passing in the :selected option.
642
+ #
643
+ # Examples:
644
+ #
645
+ # f.input :author, :selected => current_user.id
646
+ # f.input :author, :value_method => :login, :selected => current_user.login
647
+ # f.input :authors, :value_method => :login, :selected => Author.most_popular.collect(&:id)
648
+ # f.input :authors, :value_method => :login, :selected => nil # override any defaults: select none
649
+ #
650
+ # You can pass html_options to the select tag using :input_html => {}
651
+ #
652
+ # Examples:
653
+ #
654
+ # f.input :authors, :input_html => {:size => 20, :multiple => true}
655
+ #
656
+ # By default, all select inputs will have a blank option at the top of the list. You can add
657
+ # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
658
+ #
659
+ #
660
+ # You can group the options in optgroup elements by passing the :group_by option
661
+ # (Note: only tested for belongs_to relations)
662
+ #
663
+ # Examples:
664
+ #
665
+ # f.input :author, :group_by => :continent
666
+ #
667
+ # All the other options should work as expected. If you want to call a custom method on the
668
+ # group item. You can include the option:group_label_method
669
+ # Examples:
670
+ #
671
+ # f.input :author, :group_by => :continents, :group_label_method => :something_different
672
+ #
673
+ def select_input(method, options)
674
+ html_options = options.delete(:input_html) || {}
675
+ options = set_include_blank(options)
676
+ html_options[:multiple] = options.delete(:multiple) if html_options[:multiple].nil?
677
+
678
+ reflection = self.reflection_for(method)
679
+ if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
680
+ options[:include_blank] = false
681
+ html_options[:multiple] = true if html_options[:multiple].nil?
682
+ html_options[:size] ||= 5
683
+ end
684
+ options[:selected] = options[:selected].first if options[:selected].present? && html_options[:multiple] == false
685
+ input_name = generate_association_input_name(method)
686
+
687
+ select_html = if options[:group_by]
688
+ # The grouped_options_select is a bit counter intuitive and not optimised (mostly due to ActiveRecord).
689
+ # The formtastic user however shouldn't notice this too much.
690
+ raw_collection = find_raw_collection_for_column(method, options.reverse_merge(:find_options => { :include => options[:group_by] }))
691
+ label, value = detect_label_and_value_method!(raw_collection)
692
+ group_collection = raw_collection.map { |option| option.send(options[:group_by]) }.uniq
693
+ group_label_method = options[:group_label_method] || detect_label_method(group_collection)
694
+ group_collection = group_collection.sort_by { |group_item| group_item.send(group_label_method) }
695
+ group_association = options[:group_association] || detect_group_association(method, options[:group_by])
696
+
697
+ # Here comes the monster with 8 arguments
698
+ self.grouped_collection_select(input_name, group_collection,
699
+ group_association, group_label_method,
700
+ value, label,
701
+ strip_formtastic_options(options), html_options)
702
+ else
703
+ collection = find_collection_for_column(method, options)
704
+ self.select(input_name, collection, strip_formtastic_options(options), html_options)
705
+ end
706
+
707
+ self.label(method, options_for_label(options).merge(:input_name => input_name)) << select_html
708
+ end
709
+ alias :boolean_select_input :select_input
710
+
711
+ # Outputs a timezone select input as Rails' time_zone_select helper. You
712
+ # can give priority zones as option.
713
+ #
714
+ # Examples:
715
+ #
716
+ # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
717
+ #
718
+ # You can pre-select a specific option value by passing in the :selected option.
719
+ # Note: Right now only works if the form object attribute value is not set (nil),
720
+ # because of how the core helper is implemented.
721
+ #
722
+ # Examples:
723
+ #
724
+ # f.input :my_favorite_time_zone, :as => :time_zone, :selected => 'Singapore'
725
+ #
726
+ def time_zone_input(method, options)
727
+ html_options = options.delete(:input_html) || {}
728
+ selected_value = options.delete(:selected)
729
+
730
+ self.label(method, options_for_label(options)) <<
731
+ self.time_zone_select(method, options.delete(:priority_zones),
732
+ strip_formtastic_options(options).merge(:default => selected_value), html_options)
700
733
  end
701
-
702
- self.label(method, options_for_label(options).merge(:input_name => input_name)) << select_html
703
- end
704
- alias :boolean_select_input :select_input
705
734
 
706
- # Outputs a timezone select input as Rails' time_zone_select helper. You
707
- # can give priority zones as option.
708
- #
709
- # Examples:
710
- #
711
- # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
712
- #
713
- def time_zone_input(method, options)
714
- html_options = options.delete(:input_html) || {}
735
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
736
+ # items, one for each possible choice in the belongs_to association. Each li contains a
737
+ # label and a radio input.
738
+ #
739
+ # Example:
740
+ #
741
+ # f.input :author, :as => :radio
742
+ #
743
+ # Output:
744
+ #
745
+ # <fieldset>
746
+ # <legend><span>Author</span></legend>
747
+ # <ol>
748
+ # <li>
749
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
750
+ # </li>
751
+ # <li>
752
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
753
+ # </li>
754
+ # </ol>
755
+ # </fieldset>
756
+ #
757
+ # You can customize the choices available in the radio button set by passing in a collection (an Array or
758
+ # Hash) through the :collection option. If not provided, the choices are found by reflecting on the association
759
+ # (Author.find(:all) in the example above).
760
+ #
761
+ # Examples:
762
+ #
763
+ # f.input :author, :as => :radio, :collection => @authors
764
+ # f.input :author, :as => :radio, :collection => Author.find(:all)
765
+ # f.input :author, :as => :radio, :collection => [@justin, @kate]
766
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
767
+ #
768
+ # The :label_method option allows you to customize the label for each radio button two ways:
769
+ #
770
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
771
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
772
+ #
773
+ # Examples:
774
+ #
775
+ # f.input :author, :as => :radio, :label_method => :full_name
776
+ # f.input :author, :as => :radio, :label_method => :login
777
+ # f.input :author, :as => :radio, :label_method => :full_name_with_post_count
778
+ # f.input :author, :as => :radio, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
779
+ #
780
+ # The :value_method option provides the same customization of the value attribute of each option tag.
781
+ #
782
+ # Examples:
783
+ #
784
+ # f.input :author, :as => :radio, :value_method => :full_name
785
+ # f.input :author, :as => :radio, :value_method => :login
786
+ # f.input :author, :as => :radio, :value_method => Proc.new { |a| "author_#{a.login}" }
787
+ #
788
+ # You can force a particular radio button in the collection to be checked with the :selected option.
789
+ #
790
+ # Examples:
791
+ #
792
+ # f.input :subscribe_to_newsletter, :as => :radio, :selected => true
793
+ # f.input :subscribe_to_newsletter, :as => :radio, :collection => ["Yeah!", "Nope!"], :selected => "Nope!"
794
+ #
795
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each radio
796
+ # button / label combination to contain a class with the value of the radio button (useful for
797
+ # applying specific CSS or Javascript to a particular radio button).
798
+ #
799
+ def radio_input(method, options)
800
+ collection = find_collection_for_column(method, options)
801
+ html_options = strip_formtastic_options(options).merge(options.delete(:input_html) || {})
802
+
803
+ input_name = generate_association_input_name(method)
804
+ value_as_class = options.delete(:value_as_class)
805
+ input_ids = []
806
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
807
+ selected_value = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
808
+
809
+ list_item_content = collection.map do |c|
810
+ label = c.is_a?(Array) ? c.first : c
811
+ value = c.is_a?(Array) ? c.last : c
812
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
813
+ input_ids << input_id
814
+
815
+ html_options[:checked] = selected_value == value if selected_option_is_present
816
+
817
+ li_content = template.content_tag(:label,
818
+ "#{self.radio_button(input_name, value, html_options)} #{label}",
819
+ :for => input_id
820
+ )
715
821
 
716
- self.label(method, options_for_label(options)) <<
717
- self.time_zone_select(method, options.delete(:priority_zones), set_options(options), html_options)
718
- end
822
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
823
+ template.content_tag(:li, li_content, li_options)
824
+ end
719
825
 
720
- # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
721
- # items, one for each possible choice in the belongs_to association. Each li contains a
722
- # label and a radio input.
723
- #
724
- # Example:
725
- #
726
- # f.input :author, :as => :radio
727
- #
728
- # Output:
729
- #
730
- # <fieldset>
731
- # <legend><span>Author</span></legend>
732
- # <ol>
733
- # <li>
734
- # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
735
- # </li>
736
- # <li>
737
- # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
738
- # </li>
739
- # </ol>
740
- # </fieldset>
741
- #
742
- # You can customize the choices available in the radio button set by passing in a collection (an Array or
743
- # Hash) through the :collection option. If not provided, the choices are found by reflecting on the association
744
- # (Author.find(:all) in the example above).
745
- #
746
- # Examples:
747
- #
748
- # f.input :author, :as => :radio, :collection => @authors
749
- # f.input :author, :as => :radio, :collection => Author.find(:all)
750
- # f.input :author, :as => :radio, :collection => [@justin, @kate]
751
- # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
752
- #
753
- # The :label_method option allows you to customize the label for each radio button two ways:
754
- #
755
- # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
756
- # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
757
- #
758
- # Examples:
759
- #
760
- # f.input :author, :as => :radio, :label_method => :full_name
761
- # f.input :author, :as => :radio, :label_method => :login
762
- # f.input :author, :as => :radio, :label_method => :full_name_with_post_count
763
- # f.input :author, :as => :radio, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
764
- #
765
- # The :value_method option provides the same customization of the value attribute of each option tag.
766
- #
767
- # Examples:
768
- #
769
- # f.input :author, :as => :radio, :value_method => :full_name
770
- # f.input :author, :as => :radio, :value_method => :login
771
- # f.input :author, :as => :radio, :value_method => Proc.new { |a| "author_#{a.login}" }
772
- #
773
- # You can force a particular radio button in the collection to be checked with the :selected option. Example:
774
- #
775
- # f.input :subscribe_to_newsletter, :as => :radio, :selected => true
776
- # f.input :subscribe_to_newsletter, :as => :radio, :collection => ["Yeah!", "Nope!"], :selected => "Nope!"
777
- #
778
- # Finally, you can set :value_as_class => true if you want the li wrapper around each radio
779
- # button / label combination to contain a class with the value of the radio button (useful for
780
- # applying specific CSS or Javascript to a particular radio button).
781
- def radio_input(method, options)
782
- collection = find_collection_for_column(method, options)
783
- html_options = set_options(options).merge(options.delete(:input_html) || {})
784
-
785
- input_name = generate_association_input_name(method)
786
- value_as_class = options.delete(:value_as_class)
787
- input_ids = []
788
-
789
- list_item_content = collection.map do |c|
790
- label = c.is_a?(Array) ? c.first : c
791
- value = c.is_a?(Array) ? c.last : c
792
- html_options[:checked] = options.delete(:selected) unless options[:selected].blank?
793
-
794
- input_ids << input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
795
- li_content = template.content_tag(:label,
796
- "#{self.radio_button(input_name, value, html_options)} #{label}",
797
- :for => input_id
798
- )
826
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
827
+ end
828
+ alias :boolean_radio_input :radio_input
829
+
830
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
831
+ # items (li), one for each fragment for the date (year, month, day). Each li contains a label
832
+ # (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
833
+ #
834
+ # You can pre-select a specific option value by passing in the :selected option.
835
+ #
836
+ # Examples:
837
+ #
838
+ # f.input :created_at, :as => :date, :selected => 1.day.ago
839
+ # f.input :created_at, :as => :date, :selected => nil # override any defaults: select none
840
+ #
841
+ # Some of Rails' options for select_date are supported, but not everything yet.
842
+ #
843
+ def date_input(method, options)
844
+ options = set_include_blank(options)
845
+ date_or_datetime_input(method, options.merge(:discard_hour => true))
846
+ end
799
847
 
800
- li_options = value_as_class ? { :class => value.to_s.downcase } : {}
801
- template.content_tag(:li, li_content, li_options)
848
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
849
+ # items (li), one for each fragment for the date (year, month, day, hour, min, sec). Each li
850
+ # contains a label (eg "Year") and a select box. See date_or_datetime_input for a more
851
+ # detailed output example.
852
+ #
853
+ # You can pre-select a specific option value by passing in the :selected option.
854
+ #
855
+ # Examples:
856
+ #
857
+ # f.input :created_at, :as => :datetime, :selected => 1.day.ago
858
+ # f.input :created_at, :as => :datetime, :selected => nil # override any defaults: select none
859
+ #
860
+ # Some of Rails' options for select_date are supported, but not everything yet.
861
+ #
862
+ def datetime_input(method, options)
863
+ options = set_include_blank(options)
864
+ date_or_datetime_input(method, options)
802
865
  end
803
866
 
804
- field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
805
- end
806
- alias :boolean_radio_input :radio_input
867
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
868
+ # items (li), one for each fragment for the time (hour, minute, second). Each li contains a label
869
+ # (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
870
+ #
871
+ # You can pre-select a specific option value by passing in the :selected option.
872
+ #
873
+ # Examples:
874
+ #
875
+ # f.input :created_at, :as => :time, :selected => 1.hour.ago
876
+ # f.input :created_at, :as => :time, :selected => nil # override any defaults: select none
877
+ #
878
+ # Some of Rails' options for select_time are supported, but not everything yet.
879
+ #
880
+ def time_input(method, options)
881
+ options = set_include_blank(options)
882
+ date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
883
+ end
807
884
 
808
- # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
809
- # items (li), one for each fragment for the date (year, month, day). Each li contains a label
810
- # (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
811
- #
812
- # Some of Rails' options for select_date are supported, but not everything yet.
813
- def date_input(method, options)
814
- options = set_include_blank(options)
815
- date_or_datetime_input(method, options.merge(:discard_hour => true))
816
- end
885
+ # <fieldset>
886
+ # <legend>Created At</legend>
887
+ # <ol>
888
+ # <li>
889
+ # <label for="user_created_at_1i">Year</label>
890
+ # <select id="user_created_at_1i" name="user[created_at(1i)]">
891
+ # <option value="2003">2003</option>
892
+ # ...
893
+ # <option value="2013">2013</option>
894
+ # </select>
895
+ # </li>
896
+ # <li>
897
+ # <label for="user_created_at_2i">Month</label>
898
+ # <select id="user_created_at_2i" name="user[created_at(2i)]">
899
+ # <option value="1">January</option>
900
+ # ...
901
+ # <option value="12">December</option>
902
+ # </select>
903
+ # </li>
904
+ # <li>
905
+ # <label for="user_created_at_3i">Day</label>
906
+ # <select id="user_created_at_3i" name="user[created_at(3i)]">
907
+ # <option value="1">1</option>
908
+ # ...
909
+ # <option value="31">31</option>
910
+ # </select>
911
+ # </li>
912
+ # </ol>
913
+ # </fieldset>
914
+ #
915
+ # This is an absolute abomination, but so is the official Rails select_date().
916
+ #
917
+ def date_or_datetime_input(method, options)
918
+ position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
919
+ i18n_date_order = ::I18n.t(:order, :scope => [:date])
920
+ i18n_date_order = nil unless i18n_date_order.is_a?(Array)
921
+ inputs = options.delete(:order) || i18n_date_order || [:year, :month, :day]
922
+
923
+ time_inputs = [:hour, :minute]
924
+ time_inputs << [:second] if options[:include_seconds]
925
+
926
+ list_items_capture = ""
927
+ hidden_fields_capture = ""
928
+
929
+ default_time = ::Time.now
930
+
931
+ # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
932
+ datetime = options[:selected] || (@object ? @object.send(method) : default_time) || default_time
933
+
934
+ html_options = options.delete(:input_html) || {}
935
+ input_ids = []
817
936
 
818
- # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
819
- # items (li), one for each fragment for the date (year, month, day, hour, min, sec). Each li
820
- # contains a label (eg "Year") and a select box. See date_or_datetime_input for a more
821
- # detailed output example.
822
- #
823
- # Some of Rails' options for select_date are supported, but not everything yet.
824
- def datetime_input(method, options)
825
- options = set_include_blank(options)
826
- date_or_datetime_input(method, options)
827
- end
937
+ (inputs + time_inputs).each do |input|
938
+ input_ids << input_id = generate_html_id(method, "#{position[input]}i")
828
939
 
829
- # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
830
- # items (li), one for each fragment for the time (hour, minute, second). Each li contains a label
831
- # (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
832
- #
833
- # Some of Rails' options for select_time are supported, but not everything yet.
834
- def time_input(method, options)
835
- options = set_include_blank(options)
836
- date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
837
- end
940
+ field_name = "#{method}(#{position[input]}i)"
941
+ if options[:"discard_#{input}"]
942
+ break if time_inputs.include?(input)
838
943
 
839
- # <fieldset>
840
- # <legend>Created At</legend>
841
- # <ol>
842
- # <li>
843
- # <label for="user_created_at_1i">Year</label>
844
- # <select id="user_created_at_1i" name="user[created_at(1i)]">
845
- # <option value="2003">2003</option>
846
- # ...
847
- # <option value="2013">2013</option>
848
- # </select>
849
- # </li>
850
- # <li>
851
- # <label for="user_created_at_2i">Month</label>
852
- # <select id="user_created_at_2i" name="user[created_at(2i)]">
853
- # <option value="1">January</option>
854
- # ...
855
- # <option value="12">December</option>
856
- # </select>
857
- # </li>
858
- # <li>
859
- # <label for="user_created_at_3i">Day</label>
860
- # <select id="user_created_at_3i" name="user[created_at(3i)]">
861
- # <option value="1">1</option>
862
- # ...
863
- # <option value="31">31</option>
864
- # </select>
865
- # </li>
866
- # </ol>
867
- # </fieldset>
868
- #
869
- # This is an absolute abomination, but so is the official Rails select_date().
870
- #
871
- def date_or_datetime_input(method, options)
872
- position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
873
- i18n_date_order = ::I18n.t(:order, :scope => [:date])
874
- i18n_date_order = nil unless i18n_date_order.is_a?(Array)
875
- inputs = options.delete(:order) || i18n_date_order || [:year, :month, :day]
944
+ hidden_value = datetime.respond_to?(input) ? datetime.send(input.to_sym) : datetime
945
+ hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => input_id)
946
+ else
947
+ opts = strip_formtastic_options(options).merge(:prefix => @object_name, :field_name => field_name, :default => datetime)
948
+ item_label_text = ::I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
876
949
 
877
- time_inputs = [:hour, :minute]
878
- time_inputs << [:second] if options[:include_seconds]
950
+ list_items_capture << template.content_tag(:li,
951
+ template.content_tag(:label, item_label_text, :for => input_id) <<
952
+ template.send(:"select_#{input}", datetime, opts, html_options.merge(:id => input_id))
953
+ )
954
+ end
955
+ end
879
956
 
880
- list_items_capture = ""
881
- hidden_fields_capture = ""
957
+ hidden_fields_capture << field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_items_capture)
958
+ end
882
959
 
883
- # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
884
- datetime = @object ? @object.send(method) : nil
885
- html_options = options.delete(:input_html) || {}
886
- input_ids = []
960
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
961
+ # items, one for each possible choice in the belongs_to association. Each li contains a
962
+ # label and a check_box input.
963
+ #
964
+ # This is an alternative for has many and has and belongs to many associations.
965
+ #
966
+ # Example:
967
+ #
968
+ # f.input :author, :as => :check_boxes
969
+ #
970
+ # Output:
971
+ #
972
+ # <fieldset>
973
+ # <legend class="label"><label>Authors</label></legend>
974
+ # <ol>
975
+ # <li>
976
+ # <input type="hidden" name="book[author_id][1]" value="">
977
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
978
+ # </li>
979
+ # <li>
980
+ # <input type="hidden" name="book[author_id][2]" value="">
981
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
982
+ # </li>
983
+ # </ol>
984
+ # </fieldset>
985
+ #
986
+ # Notice that the value of the checkbox is the same as the id and the hidden
987
+ # field has empty value. You can override the hidden field value using the
988
+ # unchecked_value option.
989
+ #
990
+ # You can customize the options available in the set by passing in a collection (Array) of
991
+ # ActiveRecord objects through the :collection option. If not provided, the choices are found
992
+ # by inferring the parent's class name from the method name and simply calling find(:all) on
993
+ # it (Author.find(:all) in the example above).
994
+ #
995
+ # Examples:
996
+ #
997
+ # f.input :author, :as => :check_boxes, :collection => @authors
998
+ # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
999
+ # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
1000
+ #
1001
+ # The :label_method option allows you to customize the label for each checkbox two ways:
1002
+ #
1003
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
1004
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
1005
+ #
1006
+ # Examples:
1007
+ #
1008
+ # f.input :author, :as => :check_boxes, :label_method => :full_name
1009
+ # f.input :author, :as => :check_boxes, :label_method => :login
1010
+ # f.input :author, :as => :check_boxes, :label_method => :full_name_with_post_count
1011
+ # f.input :author, :as => :check_boxes, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
1012
+ #
1013
+ # The :value_method option provides the same customization of the value attribute of each checkbox input tag.
1014
+ #
1015
+ # Examples:
1016
+ #
1017
+ # f.input :author, :as => :check_boxes, :value_method => :full_name
1018
+ # f.input :author, :as => :check_boxes, :value_method => :login
1019
+ # f.input :author, :as => :check_boxes, :value_method => Proc.new { |a| "author_#{a.login}" }
1020
+ #
1021
+ # You can pre-select/check a specific checkbox value by passing in the :selected option (alias :checked works as well).
1022
+ #
1023
+ # Examples:
1024
+ #
1025
+ # f.input :authors, :as => :check_boxes, :selected => @justin
1026
+ # f.input :authors, :as => :check_boxes, :selected => Author.most_popular.collect(&:id)
1027
+ # f.input :authors, :as => :check_boxes, :selected => nil # override any defaults: select none
1028
+ #
1029
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each checkbox / label
1030
+ # combination to contain a class with the value of the radio button (useful for applying specific
1031
+ # CSS or Javascript to a particular checkbox).
1032
+ #
1033
+ def check_boxes_input(method, options)
1034
+ collection = find_collection_for_column(method, options)
1035
+ html_options = options.delete(:input_html) || {}
887
1036
 
888
- (inputs + time_inputs).each do |input|
889
- input_ids << input_id = generate_html_id(method, "#{position[input]}i")
890
-
891
- field_name = "#{method}(#{position[input]}i)"
892
- if options[:"discard_#{input}"]
893
- break if time_inputs.include?(input)
894
-
895
- hidden_value = datetime.respond_to?(input) ? datetime.send(input.to_sym) : datetime
896
- hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => input_id)
897
- else
898
- opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name)
899
- item_label_text = ::I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
1037
+ input_name = generate_association_input_name(method)
1038
+ value_as_class = options.delete(:value_as_class)
1039
+ unchecked_value = options.delete(:unchecked_value) || ''
1040
+ html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
1041
+ input_ids = []
900
1042
 
901
- list_items_capture << template.content_tag(:li,
902
- template.content_tag(:label, item_label_text, :for => input_id) <<
903
- template.send(:"select_#{input}", datetime, opts, html_options.merge(:id => input_id))
1043
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
1044
+ selected_values = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
1045
+ selected_values = [*selected_values].compact
1046
+
1047
+ list_item_content = collection.map do |c|
1048
+ label = c.is_a?(Array) ? c.first : c
1049
+ value = c.is_a?(Array) ? c.last : c
1050
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
1051
+ input_ids << input_id
1052
+
1053
+ html_options[:checked] = selected_values.include?(value) if selected_option_is_present
1054
+ html_options[:id] = input_id
1055
+
1056
+ li_content = template.content_tag(:label,
1057
+ "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
1058
+ :for => input_id
904
1059
  )
905
- end
906
- end
907
1060
 
908
- hidden_fields_capture << field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_items_capture)
909
- end
910
-
911
- # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
912
- # items, one for each possible choice in the belongs_to association. Each li contains a
913
- # label and a check_box input.
914
- #
915
- # This is an alternative for has many and has and belongs to many associations.
916
- #
917
- # Example:
918
- #
919
- # f.input :author, :as => :check_boxes
920
- #
921
- # Output:
922
- #
923
- # <fieldset>
924
- # <legend class="label"><label>Authors</label></legend>
925
- # <ol>
926
- # <li>
927
- # <input type="hidden" name="book[author_id][1]" value="">
928
- # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
929
- # </li>
930
- # <li>
931
- # <input type="hidden" name="book[author_id][2]" value="">
932
- # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
933
- # </li>
934
- # </ol>
935
- # </fieldset>
936
- #
937
- # Notice that the value of the checkbox is the same as the id and the hidden
938
- # field has empty value. You can override the hidden field value using the
939
- # unchecked_value option.
940
- #
941
- # You can customize the options available in the set by passing in a collection (Array) of
942
- # ActiveRecord objects through the :collection option. If not provided, the choices are found
943
- # by inferring the parent's class name from the method name and simply calling find(:all) on
944
- # it (Author.find(:all) in the example above).
945
- #
946
- # Examples:
947
- #
948
- # f.input :author, :as => :check_boxes, :collection => @authors
949
- # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
950
- # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
951
- #
952
- # The :label_method option allows you to customize the label for each checkbox two ways:
953
- #
954
- # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
955
- # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
956
- #
957
- # Examples:
958
- #
959
- # f.input :author, :as => :check_boxes, :label_method => :full_name
960
- # f.input :author, :as => :check_boxes, :label_method => :login
961
- # f.input :author, :as => :check_boxes, :label_method => :full_name_with_post_count
962
- # f.input :author, :as => :check_boxes, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
963
- #
964
- # The :value_method option provides the same customization of the value attribute of each checkbox input tag.
965
- #
966
- # Examples:
967
- #
968
- # f.input :author, :as => :check_boxes, :value_method => :full_name
969
- # f.input :author, :as => :check_boxes, :value_method => :login
970
- # f.input :author, :as => :check_boxes, :value_method => Proc.new { |a| "author_#{a.login}" }
971
- #
972
- # Finally, you can set :value_as_class => true if you want the li wrapper around each checkbox / label
973
- # combination to contain a class with the value of the radio button (useful for applying specific
974
- # CSS or Javascript to a particular checkbox).
975
- def check_boxes_input(method, options)
976
- collection = find_collection_for_column(method, options)
977
- html_options = options.delete(:input_html) || {}
978
-
979
- input_name = generate_association_input_name(method)
980
- value_as_class = options.delete(:value_as_class)
981
- unchecked_value = options.delete(:unchecked_value) || ''
982
- html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
983
- input_ids = []
984
-
985
- list_item_content = collection.map do |c|
986
- label = c.is_a?(Array) ? c.first : c
987
- value = c.is_a?(Array) ? c.last : c
988
-
989
- input_ids << input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
990
- html_options.merge!(:id => input_id)
991
-
992
- li_content = template.content_tag(:label,
993
- "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
994
- :for => input_id
995
- )
1061
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
1062
+ template.content_tag(:li, li_content, li_options)
1063
+ end
996
1064
 
997
- li_options = value_as_class ? { :class => value.to_s.downcase } : {}
998
- template.content_tag(:li, li_content, li_options)
1065
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
999
1066
  end
1000
1067
 
1001
- field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
1002
- end
1003
-
1004
- # Outputs a country select input, wrapping around a regular country_select helper.
1005
- # Rails doesn't come with a country_select helper by default any more, so you'll need to install
1006
- # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
1007
- # same way.
1008
- #
1009
- # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
1010
- #
1011
- # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
1012
- # which you can change to suit your market and user base (see README for more info on config).
1013
- #
1014
- # Examples:
1015
- # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
1016
- # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
1017
- #
1018
- def country_input(method, options)
1019
- 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)
1068
+ # Outputs a country select input, wrapping around a regular country_select helper.
1069
+ # Rails doesn't come with a country_select helper by default any more, so you'll need to install
1070
+ # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
1071
+ # same way.
1072
+ #
1073
+ # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
1074
+ #
1075
+ # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
1076
+ # which you can change to suit your market and user base (see README for more info on config).
1077
+ #
1078
+ # Examples:
1079
+ # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
1080
+ # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
1081
+ #
1082
+ def country_input(method, options)
1083
+ 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)
1020
1084
 
1021
- html_options = options.delete(:input_html) || {}
1022
- priority_countries = options.delete(:priority_countries) || @@priority_countries
1085
+ html_options = options.delete(:input_html) || {}
1086
+ priority_countries = options.delete(:priority_countries) || @@priority_countries
1023
1087
 
1024
- self.label(method, options_for_label(options)) <<
1025
- self.country_select(method, priority_countries, set_options(options), html_options)
1026
- end
1088
+ self.label(method, options_for_label(options)) <<
1089
+ self.country_select(method, priority_countries, strip_formtastic_options(options), html_options)
1090
+ end
1027
1091
 
1028
- # Outputs a label containing a checkbox and the label text. The label defaults
1029
- # to the column name (method name) and can be altered with the :label option.
1030
- # :checked_value and :unchecked_value options are also available.
1031
- #
1032
- def boolean_input(method, options)
1033
- html_options = options.delete(:input_html) || {}
1092
+ # Outputs a label containing a checkbox and the label text. The label defaults
1093
+ # to the column name (method name) and can be altered with the :label option.
1094
+ # :checked_value and :unchecked_value options are also available.
1095
+ #
1096
+ # You can pre-select/check the boolean checkbox by passing in the :selected option (alias :checked works as well).
1097
+ #
1098
+ # Examples:
1099
+ #
1100
+ # f.input :allow_comments, :as => :boolean, :selected => true # override any default value: selected/checked
1101
+ #
1102
+ def boolean_input(method, options)
1103
+ html_options = options.delete(:input_html) || {}
1104
+ checked = options.key?(:checked) ? options[:checked] : options[:selected]
1105
+ html_options[:checked] = checked == true if [:selected, :checked].any? { |k| options.key?(k) }
1106
+
1107
+ input = self.check_box(method, strip_formtastic_options(options).merge(html_options),
1108
+ options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
1109
+ options = options_for_label(options)
1110
+
1111
+ # the label() method will insert this nested input into the label at the last minute
1112
+ options[:label_prefix_for_nested_input] = input
1113
+
1114
+ self.label(method, options)
1115
+ end
1034
1116
 
1035
- input = self.check_box(method, set_options(options).merge(html_options),
1036
- options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
1037
- options = options_for_label(options)
1038
-
1039
- # the label() method will insert this nested input into the label at the last minute
1040
- options[:label_prefix_for_nested_input] = input
1041
-
1042
- self.label(method, options)
1043
- end
1117
+ # Generates an input for the given method using the type supplied with :as.
1118
+ def inline_input_for(method, options)
1119
+ send(:"#{options.delete(:as)}_input", method, options)
1120
+ end
1044
1121
 
1045
- # Generates an input for the given method using the type supplied with :as.
1046
- def inline_input_for(method, options)
1047
- send(:"#{options.delete(:as)}_input", method, options)
1048
- end
1122
+ # Generates hints for the given method using the text supplied in :hint.
1123
+ #
1124
+ def inline_hints_for(method, options) #:nodoc:
1125
+ options[:hint] = localized_string(method, options[:hint], :hint)
1126
+ return if options[:hint].blank?
1127
+ template.content_tag(:p, options[:hint], :class => 'inline-hints')
1128
+ end
1049
1129
 
1050
- # Generates hints for the given method using the text supplied in :hint.
1051
- #
1052
- def inline_hints_for(method, options) #:nodoc:
1053
- options[:hint] = localized_string(method, options[:hint], :hint)
1054
- return if options[:hint].blank?
1055
- template.content_tag(:p, options[:hint], :class => 'inline-hints')
1056
- end
1130
+ # Creates an error sentence by calling to_sentence on the errors array.
1131
+ #
1132
+ def error_sentence(errors) #:nodoc:
1133
+ template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
1134
+ end
1057
1135
 
1058
- # Creates an error sentence by calling to_sentence on the errors array.
1059
- #
1060
- def error_sentence(errors) #:nodoc:
1061
- template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
1062
- end
1136
+ # Creates an error li list.
1137
+ #
1138
+ def error_list(errors) #:nodoc:
1139
+ list_elements = []
1140
+ errors.each do |error|
1141
+ list_elements << template.content_tag(:li, error.untaint)
1142
+ end
1143
+ template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
1144
+ end
1063
1145
 
1064
- # Creates an error li list.
1065
- #
1066
- def error_list(errors) #:nodoc:
1067
- list_elements = []
1068
- errors.each do |error|
1069
- list_elements << template.content_tag(:li, error.untaint)
1146
+ # Creates an error sentence containing only the first error
1147
+ #
1148
+ def error_first(errors) #:nodoc:
1149
+ template.content_tag(:p, errors.first.untaint, :class => 'inline-errors')
1070
1150
  end
1071
- template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
1072
- end
1073
1151
 
1074
- # Creates an error sentence containing only the first error
1075
- #
1076
- def error_first(errors) #:nodoc:
1077
- template.content_tag(:p, errors.first.untaint, :class => 'inline-errors')
1078
- end
1152
+ # Generates the required or optional string. If the value set is a proc,
1153
+ # it evaluates the proc first.
1154
+ #
1155
+ def required_or_optional_string(required) #:nodoc:
1156
+ string_or_proc = case required
1157
+ when true
1158
+ @@required_string
1159
+ when false
1160
+ @@optional_string
1161
+ else
1162
+ required
1163
+ end
1079
1164
 
1080
- # Generates the required or optional string. If the value set is a proc,
1081
- # it evaluates the proc first.
1082
- #
1083
- def required_or_optional_string(required) #:nodoc:
1084
- string_or_proc = case required
1085
- when true
1086
- @@required_string
1087
- when false
1088
- @@optional_string
1165
+ if string_or_proc.is_a?(Proc)
1166
+ string_or_proc.call
1089
1167
  else
1090
- required
1168
+ string_or_proc.to_s
1169
+ end
1091
1170
  end
1092
1171
 
1093
- if string_or_proc.is_a?(Proc)
1094
- string_or_proc.call
1095
- else
1096
- string_or_proc.to_s
1097
- end
1098
- end
1172
+ # Generates a fieldset and wraps the content in an ordered list. When working
1173
+ # with nested attributes (in Rails 2.3), it allows %i as interpolation option
1174
+ # in :name. So you can do:
1175
+ #
1176
+ # f.inputs :name => 'Task #%i', :for => :tasks
1177
+ #
1178
+ # or the shorter equivalent:
1179
+ #
1180
+ # f.inputs 'Task #%i', :for => :tasks
1181
+ #
1182
+ # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
1183
+ # 'Task #3' and so on.
1184
+ #
1185
+ # Note: Special case for the inline inputs (non-block):
1186
+ # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
1187
+ # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
1188
+ # f.inputs :title, :body, :author # First argument is a column => (no legend)
1189
+ #
1190
+ def field_set_and_list_wrapping(*args, &block) #:nodoc:
1191
+ contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
1192
+ html_options = args.extract_options!
1193
+
1194
+ legend = html_options.delete(:name).to_s
1195
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
1196
+ legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
1197
+
1198
+ if block_given?
1199
+ contents = if template.respond_to?(:is_haml?) && template.is_haml?
1200
+ template.capture_haml(&block)
1201
+ else
1202
+ template.capture(&block)
1203
+ end
1204
+ end
1099
1205
 
1100
- # Generates a fieldset and wraps the content in an ordered list. When working
1101
- # with nested attributes (in Rails 2.3), it allows %i as interpolation option
1102
- # in :name. So you can do:
1103
- #
1104
- # f.inputs :name => 'Task #%i', :for => :tasks
1105
- #
1106
- # or the shorter equivalent:
1107
- #
1108
- # f.inputs 'Task #%i', :for => :tasks
1109
- #
1110
- # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
1111
- # 'Task #3' and so on.
1112
- #
1113
- # Note: Special case for the inline inputs (non-block):
1114
- # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
1115
- # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
1116
- # f.inputs :title, :body, :author # First argument is a column => (no legend)
1117
- #
1118
- def field_set_and_list_wrapping(*args, &block) #:nodoc:
1119
- contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
1120
- html_options = args.extract_options!
1206
+ # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
1207
+ contents = contents.join if contents.respond_to?(:join)
1208
+ fieldset = template.content_tag(:fieldset,
1209
+ legend << template.content_tag(:ol, contents),
1210
+ html_options.except(:builder, :parent)
1211
+ )
1121
1212
 
1122
- html_options[:name] ||= html_options.delete(:title)
1123
- if html_options[:name].blank?
1124
- valid_name_classes = [::String, ::Symbol]
1125
- valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first))
1126
- html_options[:name] = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
1213
+ template.concat(fieldset) if block_given?
1214
+ fieldset
1127
1215
  end
1128
- html_options[:name] = localized_string(html_options[:name], html_options[:name], :title) if html_options[:name].is_a?(::Symbol)
1129
1216
 
1130
- legend = html_options.delete(:name).to_s
1131
- legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
1132
- legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
1217
+ def field_set_title_from_args(*args) #:nodoc:
1218
+ options = args.extract_options!
1219
+ options[:name] ||= options.delete(:title)
1220
+ title = options[:name]
1133
1221
 
1134
- if block_given?
1135
- contents = if template.respond_to?(:is_haml?) && template.is_haml?
1136
- template.capture_haml(&block)
1137
- else
1138
- template.capture(&block)
1222
+ if title.blank?
1223
+ valid_name_classes = [::String, ::Symbol]
1224
+ valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first))
1225
+ title = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
1139
1226
  end
1227
+ title = localized_string(title, title, :title) if title.is_a?(::Symbol)
1228
+ title
1140
1229
  end
1141
1230
 
1142
- # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
1143
- contents = contents.join if contents.respond_to?(:join)
1144
- fieldset = template.content_tag(:fieldset,
1145
- legend << template.content_tag(:ol, contents),
1146
- html_options.except(:builder, :parent)
1147
- )
1148
-
1149
- template.concat(fieldset) if block_given?
1150
- fieldset
1151
- end
1231
+ # Also generates a fieldset and an ordered list but with label based in
1232
+ # method. This methods is currently used by radio and datetime inputs.
1233
+ #
1234
+ def field_set_and_list_wrapping_for_method(method, options, contents) #:nodoc:
1235
+ contents = contents.join if contents.respond_to?(:join)
1236
+
1237
+ template.content_tag(:fieldset,
1238
+ template.content_tag(:legend,
1239
+ self.label(method, options_for_label(options).merge(:for => options.delete(:label_for))), :class => 'label'
1240
+ ) <<
1241
+ template.content_tag(:ol, contents)
1242
+ )
1243
+ end
1152
1244
 
1153
- # Also generates a fieldset and an ordered list but with label based in
1154
- # method. This methods is currently used by radio and datetime inputs.
1155
- #
1156
- def field_set_and_list_wrapping_for_method(method, options, contents)
1157
- contents = contents.join if contents.respond_to?(:join)
1158
-
1159
- template.content_tag(:fieldset,
1160
- template.content_tag(:legend,
1161
- self.label(method, options_for_label(options).merge(:for => options.delete(:label_for))), :class => 'label'
1162
- ) <<
1163
- template.content_tag(:ol, contents)
1164
- )
1165
- end
1245
+ # For methods that have a database column, take a best guess as to what the input method
1246
+ # should be. In most cases, it will just return the column type (eg :string), but for special
1247
+ # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
1248
+ # something different (like :password and :select).
1249
+ #
1250
+ # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
1251
+ # default is a :string, a similar behaviour to Rails' scaffolding.
1252
+ #
1253
+ def default_input_type(method, options = {}) #:nodoc:
1254
+ if column = self.column_for(method)
1255
+ # Special cases where the column type doesn't map to an input method.
1256
+ case column.type
1257
+ when :string
1258
+ return :password if method.to_s =~ /password/
1259
+ return :country if method.to_s =~ /country/
1260
+ return :time_zone if method.to_s =~ /time_zone/
1261
+ when :integer
1262
+ return :select if method.to_s =~ /_id$/
1263
+ return :numeric
1264
+ when :float, :decimal
1265
+ return :numeric
1266
+ when :timestamp
1267
+ return :datetime
1268
+ end
1269
+
1270
+ # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
1271
+ return :select if column.type == :string && options.key?(:collection)
1272
+ # Try 3: Assume the input name will be the same as the column type (e.g. string_input).
1273
+ return column.type
1274
+ else
1275
+ if @object
1276
+ return :select if self.reflection_for(method)
1166
1277
 
1167
- # For methods that have a database column, take a best guess as to what the input method
1168
- # should be. In most cases, it will just return the column type (eg :string), but for special
1169
- # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
1170
- # something different (like :password and :select).
1171
- #
1172
- # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
1173
- # default is a :string, a similar behaviour to Rails' scaffolding.
1174
- #
1175
- def default_input_type(method) #:nodoc:
1176
- column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1177
-
1178
- if column
1179
- # handle the special cases where the column type doesn't map to an input method
1180
- return :time_zone if column.type == :string && method.to_s =~ /time_zone/
1181
- return :select if column.type == :integer && method.to_s =~ /_id$/
1182
- return :datetime if column.type == :timestamp
1183
- return :numeric if [:integer, :float, :decimal].include?(column.type)
1184
- return :password if column.type == :string && method.to_s =~ /password/
1185
- return :country if column.type == :string && method.to_s =~ /country/
1186
-
1187
- # otherwise assume the input name will be the same as the column type (eg string_input)
1188
- return column.type
1189
- else
1190
- if @object
1191
- return :select if find_reflection(method)
1278
+ file = @object.send(method) if @object.respond_to?(method)
1279
+ return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
1280
+ end
1192
1281
 
1193
- file = @object.send(method) if @object.respond_to?(method)
1194
- return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
1282
+ return :select if options.key?(:collection)
1283
+ return :password if method.to_s =~ /password/
1284
+ return :string
1195
1285
  end
1286
+ end
1196
1287
 
1197
- return :password if method.to_s =~ /password/
1198
- return :string
1288
+ # Used by select and radio inputs. The collection can be retrieved by
1289
+ # three ways:
1290
+ #
1291
+ # * Explicitly provided through :collection
1292
+ # * Retrivied through an association
1293
+ # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
1294
+ #
1295
+ # If the collection is not a hash or an array of strings, fixnums or arrays,
1296
+ # we use label_method and value_method to retreive an array with the
1297
+ # appropriate label and value.
1298
+ #
1299
+ def find_collection_for_column(column, options) #:nodoc:
1300
+ collection = find_raw_collection_for_column(column, options)
1301
+
1302
+ # Return if we have an Array of strings, fixnums or arrays
1303
+ return collection if (collection.instance_of?(Array) || collection.instance_of?(Range)) &&
1304
+ [Array, Fixnum, String, Symbol].include?(collection.first.class)
1305
+
1306
+ label, value = detect_label_and_value_method!(collection, options)
1307
+ collection.map { |o| [send_or_call(label, o), send_or_call(value, o)] }
1199
1308
  end
1200
- end
1201
1309
 
1202
- # Used by select and radio inputs. The collection can be retrieved by
1203
- # three ways:
1204
- #
1205
- # * Explicitly provided through :collection
1206
- # * Retrivied through an association
1207
- # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
1208
- #
1209
- # If the collection is not a hash or an array of strings, fixnums or arrays,
1210
- # we use label_method and value_method to retreive an array with the
1211
- # appropriate label and value.
1212
- #
1213
- def find_collection_for_column(column, options)
1214
- collection = find_raw_collection_for_column(column, options)
1310
+ # As #find_collection_for_column but returns the collection without mapping the label and value
1311
+ #
1312
+ def find_raw_collection_for_column(column, options) #:nodoc:
1313
+ collection = if options[:collection]
1314
+ options.delete(:collection)
1315
+ elsif reflection = self.reflection_for(column)
1316
+ reflection.klass.find(:all, options[:find_options] || {})
1317
+ else
1318
+ create_boolean_collection(options)
1319
+ end
1215
1320
 
1216
- # Return if we have an Array of strings, fixnums or arrays
1217
- return collection if (collection.instance_of?(Array) || collection.instance_of?(Range)) &&
1218
- [Array, Fixnum, String, Symbol].include?(collection.first.class)
1321
+ collection = collection.to_a if collection.is_a?(Hash)
1322
+ collection
1323
+ end
1219
1324
 
1220
- label, value = detect_label_and_value_method!(collection, options)
1325
+ # Detects the label and value methods from a collection values set in
1326
+ # @@collection_label_methods. It will use and delete
1327
+ # the options :label_method and :value_methods when present
1328
+ #
1329
+ def detect_label_and_value_method!(collection_or_instance, options = {}) #:nodoc
1330
+ label = options.delete(:label_method) || detect_label_method(collection_or_instance)
1331
+ value = options.delete(:value_method) || :id
1332
+ [label, value]
1333
+ end
1221
1334
 
1222
- collection.map { |o| [send_or_call(label, o), send_or_call(value, o)] }
1223
- end
1224
-
1225
- # As #find_collection_for_column but returns the collection without mapping the label and value
1226
- #
1227
- def find_raw_collection_for_column(column, options) #:nodoc:
1228
- reflection = find_reflection(column)
1335
+ # Detected the label collection method when none is supplied using the
1336
+ # values set in @@collection_label_methods.
1337
+ #
1338
+ def detect_label_method(collection) #:nodoc:
1339
+ @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
1340
+ end
1229
1341
 
1230
- collection = if options[:collection]
1231
- options.delete(:collection)
1232
- elsif reflection
1233
- reflection.klass.find(:all, options[:find_options] || {})
1234
- else
1235
- create_boolean_collection(options)
1342
+ # Detects the method to call for fetching group members from the groups when grouping select options
1343
+ #
1344
+ def detect_group_association(method, group_by)
1345
+ object_to_method_reflection = self.reflection_for(method)
1346
+ method_class = object_to_method_reflection.klass
1347
+
1348
+ method_to_group_association = method_class.reflect_on_association(group_by)
1349
+ group_class = method_to_group_association.klass
1350
+
1351
+ # This will return in the normal case
1352
+ return method.to_s.pluralize.to_sym if group_class.reflect_on_association(method.to_s.pluralize)
1353
+
1354
+ # This is for belongs_to associations named differently than their class
1355
+ # form.input :parent, :group_by => :customer
1356
+ # eg.
1357
+ # class Project
1358
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1359
+ # belongs_to :customer
1360
+ # end
1361
+ # class Customer
1362
+ # has_many :projects
1363
+ # end
1364
+ group_method = method_class.to_s.underscore.pluralize.to_sym
1365
+ return group_method if group_class.reflect_on_association(group_method) # :projects
1366
+
1367
+ # This is for has_many associations named differently than their class
1368
+ # eg.
1369
+ # class Project
1370
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1371
+ # belongs_to :customer
1372
+ # end
1373
+ # class Customer
1374
+ # has_many :tasks, :class_name => 'Project', :foreign_key => 'customer_id'
1375
+ # end
1376
+ possible_associations = group_class.reflect_on_all_associations(:has_many).find_all{|assoc| assoc.klass == object_class}
1377
+ return possible_associations.first.name.to_sym if possible_associations.count == 1
1378
+
1379
+ raise "Cannot infer group association for #{method} grouped by #{group_by}, there were #{possible_associations.empty? ? 'no' : possible_associations.size} possible associations. Please specify using :group_association"
1380
+
1236
1381
  end
1237
1382
 
1238
- collection = collection.to_a if collection.is_a?(Hash)
1383
+ # Returns a hash to be used by radio and select inputs when a boolean field
1384
+ # is provided.
1385
+ #
1386
+ def create_boolean_collection(options) #:nodoc:
1387
+ options[:true] ||= ::Formtastic::I18n.t(:yes)
1388
+ options[:false] ||= ::Formtastic::I18n.t(:no)
1389
+ options[:value_as_class] = true unless options.key?(:value_as_class)
1239
1390
 
1240
- collection
1241
- end
1242
-
1243
- # Detects the label and value methods from a collection values set in
1244
- # @@collection_label_methods. It will use and delete
1245
- # the options :label_method and :value_methods when present
1246
- #
1247
- def detect_label_and_value_method!(collection_or_instance, options = {}) #:nodoc
1248
- label = options.delete(:label_method) || detect_label_method(collection_or_instance)
1249
- value = options.delete(:value_method) || :id
1250
- [label, value]
1251
- end
1391
+ [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
1392
+ end
1252
1393
 
1253
- # Detected the label collection method when none is supplied using the
1254
- # values set in @@collection_label_methods.
1255
- #
1256
- def detect_label_method(collection) #:nodoc:
1257
- @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
1258
- end
1394
+ # Used by association inputs (select, radio) to generate the name that should
1395
+ # be used for the input
1396
+ #
1397
+ # belongs_to :author; f.input :author; will generate 'author_id'
1398
+ # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
1399
+ # has_many :authors; f.input :authors; will generate 'author_ids'
1400
+ # has_and_belongs_to_many will act like has_many
1401
+ #
1402
+ def generate_association_input_name(method) #:nodoc:
1403
+ if reflection = self.reflection_for(method)
1404
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1405
+ "#{method.to_s.singularize}_ids"
1406
+ else
1407
+ reflection.options[:foreign_key] || "#{method}_id"
1408
+ end
1409
+ else
1410
+ method
1411
+ end.to_sym
1412
+ end
1259
1413
 
1260
- # Returns a hash to be used by radio and select inputs when a boolean field
1261
- # is provided.
1262
- #
1263
- def create_boolean_collection(options)
1264
- options[:true] ||= ::Formtastic::I18n.t(:yes)
1265
- options[:false] ||= ::Formtastic::I18n.t(:no)
1266
- options[:value_as_class] = true unless options.key?(:value_as_class)
1414
+ # If an association method is passed in (f.input :author) try to find the
1415
+ # reflection object.
1416
+ #
1417
+ def reflection_for(method) #:nodoc:
1418
+ @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
1419
+ end
1267
1420
 
1268
- [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
1269
- end
1421
+ # Get a column object for a specified attribute method - if possible.
1422
+ #
1423
+ def column_for(method) #:nodoc:
1424
+ @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1425
+ end
1270
1426
 
1271
- # Used by association inputs (select, radio) to generate the name that should
1272
- # be used for the input
1273
- #
1274
- # belongs_to :author; f.input :author; will generate 'author_id'
1275
- # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
1276
- # has_many :authors; f.input :authors; will generate 'author_ids'
1277
- # has_and_belongs_to_many will act like has_many
1278
- #
1279
- def generate_association_input_name(method)
1280
- if reflection = find_reflection(method)
1281
- if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1282
- "#{method.to_s.singularize}_ids"
1427
+ # Generates default_string_options by retrieving column information from
1428
+ # the database.
1429
+ #
1430
+ def default_string_options(method, type) #:nodoc:
1431
+ column = self.column_for(method)
1432
+
1433
+ if type == :numeric || column.nil? || column.limit.nil?
1434
+ { :size => @@default_text_field_size }
1283
1435
  else
1284
- reflection.options[:foreign_key] || "#{method}_id"
1436
+ { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
1285
1437
  end
1286
- else
1287
- method
1288
- end.to_sym
1289
- end
1290
-
1291
- # If an association method is passed in (f.input :author) try to find the
1292
- # reflection object.
1293
- #
1294
- def find_reflection(method)
1295
- @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
1296
- end
1438
+ end
1297
1439
 
1298
- # Generates default_string_options by retrieving column information from
1299
- # the database.
1300
- #
1301
- def default_string_options(method, type) #:nodoc:
1302
- column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1440
+ # Generate the html id for the li tag.
1441
+ # It takes into account options[:index] and @auto_index to generate li
1442
+ # elements with appropriate index scope. It also sanitizes the object
1443
+ # and method names.
1444
+ #
1445
+ def generate_html_id(method_name, value='input') #:nodoc:
1446
+ if options.has_key?(:index)
1447
+ index = "_#{options[:index]}"
1448
+ elsif defined?(@auto_index)
1449
+ index = "_#{@auto_index}"
1450
+ else
1451
+ index = ""
1452
+ end
1453
+ sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
1303
1454
 
1304
- if type == :numeric || column.nil? || column.limit.nil?
1305
- { :size => @@default_text_field_size }
1306
- else
1307
- { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
1455
+ "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
1308
1456
  end
1309
- end
1310
1457
 
1311
- # Generate the html id for the li tag.
1312
- # It takes into account options[:index] and @auto_index to generate li
1313
- # elements with appropriate index scope. It also sanitizes the object
1314
- # and method names.
1315
- #
1316
- def generate_html_id(method_name, value='input')
1317
- if options.has_key?(:index)
1318
- index = "_#{options[:index]}"
1319
- elsif defined?(@auto_index)
1320
- index = "_#{@auto_index}"
1321
- else
1322
- index = ""
1458
+ # Gets the nested_child_index value from the parent builder. In Rails 2.3
1459
+ # it always returns a fixnum. In next versions it returns a hash with each
1460
+ # association that the parent builds.
1461
+ #
1462
+ def parent_child_index(parent) #:nodoc:
1463
+ duck = parent[:builder].instance_variable_get('@nested_child_index')
1464
+
1465
+ if duck.is_a?(Hash)
1466
+ child = parent[:for]
1467
+ child = child.first if child.respond_to?(:first)
1468
+ duck[child].to_i + 1
1469
+ else
1470
+ duck.to_i + 1
1471
+ end
1323
1472
  end
1324
- sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
1325
-
1326
- "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
1327
- end
1328
1473
 
1329
- # Gets the nested_child_index value from the parent builder. In Rails 2.3
1330
- # it always returns a fixnum. In next versions it returns a hash with each
1331
- # association that the parent builds.
1332
- #
1333
- def parent_child_index(parent)
1334
- duck = parent[:builder].instance_variable_get('@nested_child_index')
1335
-
1336
- if duck.is_a?(Hash)
1337
- child = parent[:for]
1338
- child = child.first if child.respond_to?(:first)
1339
- duck[child].to_i + 1
1340
- else
1341
- duck.to_i + 1
1474
+ def sanitized_object_name #:nodoc:
1475
+ @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
1342
1476
  end
1343
- end
1344
-
1345
- def sanitized_object_name
1346
- @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
1347
- end
1348
1477
 
1349
- def humanized_attribute_name(method)
1350
- if @object && @object.class.respond_to?(:human_attribute_name)
1351
- @object.class.human_attribute_name(method.to_s)
1352
- else
1478
+ def humanized_attribute_name(method) #:nodoc:
1353
1479
  method.to_s.send(@@label_str_method)
1354
1480
  end
1355
- end
1356
-
1357
- # Internal generic method for looking up localized values within Formtastic
1358
- # using I18n, if no explicit value is set and I18n-lookups are enabled.
1359
- #
1360
- # Enabled/Disable this by setting:
1361
- #
1362
- # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
1363
- #
1364
- # Lookup priority:
1365
- #
1366
- # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
1367
- # 'formtastic.{{type}}.{{model}}.{{attribute}}'
1368
- # 'formtastic.{{type}}.{{attribute}}'
1369
- #
1370
- # Example:
1371
- #
1372
- # 'formtastic.labels.post.edit.title'
1373
- # 'formtastic.labels.post.title'
1374
- # 'formtastic.labels.title'
1375
- #
1376
- # NOTE: Generic, but only used for form input titles/labels/hints/actions (titles = legends, actions = buttons).
1377
- #
1378
- def localized_string(key, value, type, options = {})
1379
- key = value if value.is_a?(::Symbol)
1380
1481
 
1381
- if value.is_a?(::String)
1382
- value
1383
- else
1384
- use_i18n = value.nil? ? @@i18n_lookups_by_default : (value != false)
1385
-
1386
- if use_i18n
1387
- model_name = (@object ? @object.class.name : @object_name.to_s.send(@@label_str_method)).underscore
1388
- action_name = template.params[:action].to_s rescue ''
1389
- attribute_name = key.to_s
1390
-
1391
- defaults = I18N_SCOPES.collect do |i18n_scope|
1392
- i18n_path = i18n_scope.dup
1393
- i18n_path.gsub!('{{action}}', action_name)
1394
- i18n_path.gsub!('{{model}}', model_name)
1395
- i18n_path.gsub!('{{attribute}}', attribute_name)
1396
- i18n_path.gsub!('..', '.')
1397
- i18n_path.to_sym
1482
+ # Internal generic method for looking up localized values within Formtastic
1483
+ # using I18n, if no explicit value is set and I18n-lookups are enabled.
1484
+ #
1485
+ # Enabled/Disable this by setting:
1486
+ #
1487
+ # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
1488
+ #
1489
+ # Lookup priority:
1490
+ #
1491
+ # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
1492
+ # 'formtastic.{{type}}.{{model}}.{{attribute}}'
1493
+ # 'formtastic.{{type}}.{{attribute}}'
1494
+ #
1495
+ # Example:
1496
+ #
1497
+ # 'formtastic.labels.post.edit.title'
1498
+ # 'formtastic.labels.post.title'
1499
+ # 'formtastic.labels.title'
1500
+ #
1501
+ # NOTE: Generic, but only used for form input titles/labels/hints/actions (titles = legends, actions = buttons).
1502
+ #
1503
+ def localized_string(key, value, type, options = {}) #:nodoc:
1504
+ key = value if value.is_a?(::Symbol)
1505
+
1506
+ if value.is_a?(::String)
1507
+ value
1508
+ else
1509
+ use_i18n = value.nil? ? @@i18n_lookups_by_default : (value != false)
1510
+
1511
+ if use_i18n
1512
+ model_name = self.model_name.underscore
1513
+ action_name = template.params[:action].to_s rescue ''
1514
+ attribute_name = key.to_s
1515
+
1516
+ defaults = ::Formtastic::I18n::SCOPES.collect do |i18n_scope|
1517
+ i18n_path = i18n_scope.dup
1518
+ i18n_path.gsub!('{{action}}', action_name)
1519
+ i18n_path.gsub!('{{model}}', model_name)
1520
+ i18n_path.gsub!('{{attribute}}', attribute_name)
1521
+ i18n_path.gsub!('..', '.')
1522
+ i18n_path.to_sym
1523
+ end
1524
+ defaults << ''
1525
+
1526
+ i18n_value = ::Formtastic::I18n.t(defaults.shift,
1527
+ options.merge(:default => defaults, :scope => type.to_s.pluralize.to_sym))
1528
+ i18n_value.blank? ? nil : i18n_value
1398
1529
  end
1399
- defaults << ''
1400
-
1401
- i18n_value = ::Formtastic::I18n.t(defaults.shift,
1402
- options.merge(:default => defaults,:scope => type.to_s.pluralize.to_sym))
1403
- i18n_value.blank? ? nil : i18n_value
1404
1530
  end
1405
1531
  end
1406
- end
1407
1532
 
1408
- def send_or_call(duck, object)
1409
- if duck.is_a?(Proc)
1410
- duck.call(object)
1411
- else
1412
- object.send(duck)
1533
+ def model_name
1534
+ @object.present? ? @object.class.name : @object_name.to_s.classify
1413
1535
  end
1414
- end
1415
1536
 
1416
- def set_include_blank(options)
1417
- unless options.key?(:include_blank) || options.key?(:prompt)
1418
- options[:include_blank] = @@include_blank_for_select_by_default
1537
+ def send_or_call(duck, object)
1538
+ if duck.is_a?(Proc)
1539
+ duck.call(object)
1540
+ else
1541
+ object.send(duck)
1542
+ end
1543
+ end
1544
+
1545
+ def set_include_blank(options)
1546
+ unless options.key?(:include_blank) || options.key?(:prompt)
1547
+ options[:include_blank] = @@include_blank_for_select_by_default
1548
+ end
1549
+ options
1419
1550
  end
1420
- options
1421
- end
1422
1551
 
1423
1552
  end
1424
1553
 
@@ -1480,7 +1609,7 @@ module Formtastic #:nodoc:
1480
1609
  src = <<-END_SRC
1481
1610
  def semantic_#{meth}(record_or_name_or_array, *args, &proc)
1482
1611
  options = args.extract_options!
1483
- options[:builder] = @@builder
1612
+ options[:builder] ||= @@builder
1484
1613
  options[:html] ||= {}
1485
1614
 
1486
1615
  class_names = options[:html][:class] ? options[:html][:class].split(" ") : []