simple_form 1.5.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. data/CHANGELOG.md +234 -0
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +816 -0
  4. data/lib/generators/simple_form/install_generator.rb +15 -1
  5. data/lib/generators/simple_form/templates/README +12 -0
  6. data/lib/generators/simple_form/templates/_form.html.erb +2 -2
  7. data/lib/generators/simple_form/templates/_form.html.haml +2 -2
  8. data/lib/generators/simple_form/templates/_form.html.slim +4 -4
  9. data/lib/generators/simple_form/templates/config/initializers/simple_form.rb.tt +176 -0
  10. data/lib/simple_form/action_view_extensions/builder.rb +206 -59
  11. data/lib/simple_form/action_view_extensions/form_helper.rb +30 -23
  12. data/lib/simple_form/components/errors.rb +6 -24
  13. data/lib/simple_form/components/hints.rb +7 -21
  14. data/lib/simple_form/components/html5.rb +26 -0
  15. data/lib/simple_form/components/labels.rb +22 -14
  16. data/lib/simple_form/components/maxlength.rb +41 -0
  17. data/lib/simple_form/components/min_max.rb +49 -0
  18. data/lib/simple_form/components/pattern.rb +34 -0
  19. data/lib/simple_form/components/placeholders.rb +5 -17
  20. data/lib/simple_form/components/readonly.rb +22 -0
  21. data/lib/simple_form/components.rb +11 -1
  22. data/lib/simple_form/core_ext/hash.rb +16 -0
  23. data/lib/simple_form/error_notification.rb +9 -3
  24. data/lib/simple_form/form_builder.rb +105 -28
  25. data/lib/simple_form/helpers/autofocus.rb +11 -0
  26. data/lib/simple_form/helpers/disabled.rb +15 -0
  27. data/lib/simple_form/helpers/readonly.rb +15 -0
  28. data/lib/simple_form/helpers/required.rb +10 -11
  29. data/lib/simple_form/helpers/validators.rb +4 -4
  30. data/lib/simple_form/helpers.rb +7 -4
  31. data/lib/simple_form/inputs/base.rb +53 -81
  32. data/lib/simple_form/inputs/boolean_input.rb +46 -4
  33. data/lib/simple_form/inputs/collection_check_boxes_input.rb +21 -0
  34. data/lib/simple_form/inputs/collection_input.rb +27 -13
  35. data/lib/simple_form/inputs/collection_radio_buttons_input.rb +67 -0
  36. data/lib/simple_form/inputs/collection_select_input.rb +14 -0
  37. data/lib/simple_form/inputs/date_time_input.rb +10 -6
  38. data/lib/simple_form/inputs/grouped_collection_select_input.rb +41 -0
  39. data/lib/simple_form/inputs/hidden_input.rb +3 -6
  40. data/lib/simple_form/inputs/numeric_input.rb +3 -51
  41. data/lib/simple_form/inputs/password_input.rb +1 -2
  42. data/lib/simple_form/inputs/priority_input.rb +2 -2
  43. data/lib/simple_form/inputs/range_input.rb +1 -3
  44. data/lib/simple_form/inputs/string_input.rb +6 -8
  45. data/lib/simple_form/inputs/text_input.rb +1 -2
  46. data/lib/simple_form/inputs.rb +17 -13
  47. data/lib/simple_form/version.rb +1 -1
  48. data/lib/simple_form/wrappers/builder.rb +103 -0
  49. data/lib/simple_form/wrappers/many.rb +69 -0
  50. data/lib/simple_form/wrappers/root.rb +34 -0
  51. data/lib/simple_form/wrappers/single.rb +18 -0
  52. data/lib/simple_form/wrappers.rb +8 -0
  53. data/lib/simple_form.rb +118 -48
  54. data/test/action_view_extensions/builder_test.rb +285 -102
  55. data/test/action_view_extensions/form_helper_test.rb +32 -10
  56. data/test/components/label_test.rb +44 -5
  57. data/test/form_builder/association_test.rb +177 -0
  58. data/test/form_builder/button_test.rb +47 -0
  59. data/test/{error_notification_test.rb → form_builder/error_notification_test.rb} +18 -1
  60. data/test/form_builder/error_test.rb +121 -0
  61. data/test/form_builder/general_test.rb +356 -0
  62. data/test/form_builder/hint_test.rb +123 -0
  63. data/test/form_builder/input_field_test.rb +63 -0
  64. data/test/form_builder/label_test.rb +65 -0
  65. data/test/form_builder/wrapper_test.rb +149 -0
  66. data/test/generators/simple_form_generator_test.rb +32 -0
  67. data/test/inputs/boolean_input_test.rb +101 -0
  68. data/test/inputs/collection_check_boxes_input_test.rb +224 -0
  69. data/test/inputs/collection_radio_buttons_input_test.rb +326 -0
  70. data/test/inputs/collection_select_input_test.rb +241 -0
  71. data/test/inputs/datetime_input_test.rb +99 -0
  72. data/test/inputs/disabled_test.rb +38 -0
  73. data/test/inputs/discovery_test.rb +61 -0
  74. data/test/inputs/file_input_test.rb +16 -0
  75. data/test/inputs/general_test.rb +69 -0
  76. data/test/inputs/grouped_collection_select_input_test.rb +118 -0
  77. data/test/inputs/hidden_input_test.rb +30 -0
  78. data/test/inputs/numeric_input_test.rb +167 -0
  79. data/test/inputs/priority_input_test.rb +43 -0
  80. data/test/inputs/readonly_test.rb +61 -0
  81. data/test/inputs/required_test.rb +113 -0
  82. data/test/inputs/string_input_test.rb +140 -0
  83. data/test/inputs/text_input_test.rb +24 -0
  84. data/test/support/misc_helpers.rb +53 -12
  85. data/test/support/mock_controller.rb +2 -2
  86. data/test/support/models.rb +20 -5
  87. data/test/test_helper.rb +11 -12
  88. metadata +124 -96
  89. data/.gitignore +0 -3
  90. data/.gitmodules +0 -3
  91. data/.travis.yml +0 -15
  92. data/CHANGELOG.rdoc +0 -159
  93. data/Gemfile +0 -9
  94. data/README.rdoc +0 -466
  95. data/Rakefile +0 -27
  96. data/lib/generators/simple_form/templates/config/initializers/simple_form.rb +0 -93
  97. data/lib/simple_form/components/wrapper.rb +0 -38
  98. data/lib/simple_form/helpers/has_errors.rb +0 -15
  99. data/lib/simple_form/helpers/maxlength.rb +0 -24
  100. data/lib/simple_form/helpers/pattern.rb +0 -28
  101. data/simple_form.gemspec +0 -25
  102. data/test/components/error_test.rb +0 -56
  103. data/test/components/hint_test.rb +0 -74
  104. data/test/components/wrapper_test.rb +0 -63
  105. data/test/custom_components.rb +0 -7
  106. data/test/form_builder_test.rb +0 -930
  107. data/test/inputs_test.rb +0 -995
  108. /data/test/{discovery_inputs.rb → support/discovery_inputs.rb} +0 -0
@@ -3,7 +3,15 @@ module SimpleForm
3
3
  class InstallGenerator < Rails::Generators::Base
4
4
  desc "Copy SimpleForm default files"
5
5
  source_root File.expand_path('../templates', __FILE__)
6
- class_option :template_engine
6
+ class_option :template_engine, :desc => 'Template engine to be invoked (erb, haml or slim).'
7
+ class_option :bootstrap, :type => :boolean, :desc => 'Add the Twitter Bootstrap wrappers to the SimpleForm initializer.'
8
+
9
+ def info_bootstrap
10
+ return if options.bootstrap?
11
+ puts "SimpleForm 2 supports Twitter bootstrap. In case you want to " \
12
+ "generate bootstrap configuration, please re-run this " \
13
+ "generator passing --bootstrap as option."
14
+ end
7
15
 
8
16
  def copy_config
9
17
  directory 'config'
@@ -13,6 +21,12 @@ module SimpleForm
13
21
  engine = options[:template_engine]
14
22
  copy_file "_form.html.#{engine}", "lib/templates/#{engine}/scaffold/_form.html.#{engine}"
15
23
  end
24
+
25
+ def show_readme
26
+ if behavior == :invoke && options.bootstrap?
27
+ readme "README"
28
+ end
29
+ end
16
30
  end
17
31
  end
18
32
  end
@@ -0,0 +1,12 @@
1
+ ===============================================================================
2
+
3
+ Be sure to have a copy of the Bootstrap stylesheet available on your
4
+ application, you can get it on http://twitter.github.com/bootstrap.
5
+
6
+ Inside your views, use the 'simple_form_for' with one of the Bootstrap form
7
+ classes, '.form-horizontal', '.form-inline', '.form-search' or
8
+ '.form-vertical', as the following:
9
+
10
+ = simple_form_for(@user, :html => {:class => 'form-horizontal' }) do |form|
11
+
12
+ ===============================================================================
@@ -1,13 +1,13 @@
1
1
  <%%= simple_form_for(@<%= singular_table_name %>) do |f| %>
2
2
  <%%= f.error_notification %>
3
3
 
4
- <div class="inputs">
4
+ <div class="form-inputs">
5
5
  <%- attributes.each do |attribute| -%>
6
6
  <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %>
7
7
  <%- end -%>
8
8
  </div>
9
9
 
10
- <div class="actions">
10
+ <div class="form-actions">
11
11
  <%%= f.button :submit %>
12
12
  </div>
13
13
  <%% end %>
@@ -1,10 +1,10 @@
1
1
  = simple_form_for(@<%= singular_table_name %>) do |f|
2
2
  = f.error_notification
3
3
 
4
- .inputs
4
+ .form-inputs
5
5
  <%- attributes.each do |attribute| -%>
6
6
  = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %>
7
7
  <%- end -%>
8
8
 
9
- .actions
9
+ .form-actions
10
10
  = f.button :submit
@@ -1,10 +1,10 @@
1
1
  = simple_form_for(@<%= singular_table_name %>) do |f|
2
2
  = f.error_notification
3
3
 
4
- .inputs
5
- | <%- attributes.each do |attribute| -%>
4
+ .form-inputs
5
+ <%- attributes.each do |attribute| -%>
6
6
  = f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %>
7
- | <%- end -%>
7
+ <%- end -%>
8
8
 
9
- .actions
9
+ .form-actions
10
10
  = f.button :submit
@@ -0,0 +1,176 @@
1
+ # Use this setup block to configure all options available in SimpleForm.
2
+ SimpleForm.setup do |config|
3
+ # Wrappers are used by the form builder to generate a
4
+ # complete input. You can remove any component from the
5
+ # wrapper, change the order or even add your own to the
6
+ # stack. The options given below are used to wrap the
7
+ # whole input.
8
+ config.wrappers :default, :class => :input,
9
+ :hint_class => :field_with_hint, :error_class => :field_with_errors do |b|
10
+ ## Extensions enabled by default
11
+ # Any of these extensions can be disabled for a
12
+ # given input by passing: `f.input EXTENSION_NAME => false`.
13
+ # You can make any of these extensions optional by
14
+ # renaming `b.use` to `b.optional`.
15
+
16
+ # Determines whether to use HTML5 (:email, :url, ...)
17
+ # and required attributes
18
+ b.use :html5
19
+
20
+ # Calculates placeholders automatically from I18n
21
+ # You can also pass a string as f.input :placeholder => "Placeholder"
22
+ b.use :placeholder
23
+
24
+ ## Optional extensions
25
+ # They are disabled unless you pass `f.input EXTENSION_NAME => :lookup`
26
+ # to the input. If so, they will retrieve the values from the model
27
+ # if any exists. If you want to enable the lookup for any of those
28
+ # extensions by default, you can change `b.optional` to `b.use`.
29
+
30
+ # Calculates maxlength from length validations for string inputs
31
+ b.optional :maxlength
32
+
33
+ # Calculates pattern from format validations for string inputs
34
+ b.optional :pattern
35
+
36
+ # Calculates min and max from length validations for numeric inputs
37
+ b.optional :min_max
38
+
39
+ # Calculates readonly automatically from readonly attributes
40
+ b.optional :readonly
41
+
42
+ ## Inputs
43
+ b.use :label_input
44
+ b.use :hint, :wrap_with => { :tag => :span, :class => :hint }
45
+ b.use :error, :wrap_with => { :tag => :span, :class => :error }
46
+ end
47
+ <% if options.bootstrap? %>
48
+ config.wrappers :bootstrap, :tag => 'div', :class => 'control-group', :error_class => 'error' do |b|
49
+ b.use :placeholder
50
+ b.use :label
51
+ b.wrapper :tag => 'div', :class => 'controls' do |ba|
52
+ ba.use :input
53
+ ba.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
54
+ ba.use :hint, :wrap_with => { :tag => 'p', :class => 'help-block' }
55
+ end
56
+ end
57
+
58
+ config.wrappers :prepend, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
59
+ b.use :placeholder
60
+ b.use :label
61
+ b.wrapper :tag => 'div', :class => 'controls' do |input|
62
+ input.wrapper :tag => 'div', :class => 'input-prepend' do |prepend|
63
+ prepend.use :input
64
+ end
65
+ input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' }
66
+ input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
67
+ end
68
+ end
69
+
70
+ config.wrappers :append, :tag => 'div', :class => "control-group", :error_class => 'error' do |b|
71
+ b.use :placeholder
72
+ b.use :label
73
+ b.wrapper :tag => 'div', :class => 'controls' do |input|
74
+ input.wrapper :tag => 'div', :class => 'input-append' do |append|
75
+ append.use :input
76
+ end
77
+ input.use :hint, :wrap_with => { :tag => 'span', :class => 'help-block' }
78
+ input.use :error, :wrap_with => { :tag => 'span', :class => 'help-inline' }
79
+ end
80
+ end
81
+
82
+ # Wrappers for forms and inputs using the Twitter Bootstrap toolkit.
83
+ # Check the Bootstrap docs (http://twitter.github.com/bootstrap)
84
+ # to learn about the different styles for forms and inputs,
85
+ # buttons and other elements.
86
+ config.default_wrapper = :bootstrap
87
+ <% else %>
88
+ # The default wrapper to be used by the FormBuilder.
89
+ config.default_wrapper = :default
90
+ <% end %>
91
+ # Define the way to render check boxes / radio buttons with labels.
92
+ # Defaults to :nested for bootstrap config.
93
+ # :inline => input + label
94
+ # :nested => label > input
95
+ config.boolean_style = :nested
96
+
97
+ # Default class for buttons
98
+ config.button_class = 'btn'
99
+
100
+ # Method used to tidy up errors.
101
+ # config.error_method = :first
102
+
103
+ # Default tag used for error notification helper.
104
+ config.error_notification_tag = :div
105
+
106
+ # CSS class to add for error notification helper.
107
+ config.error_notification_class = 'alert alert-error'
108
+
109
+ # ID to add for error notification helper.
110
+ # config.error_notification_id = nil
111
+
112
+ # Series of attempts to detect a default label method for collection.
113
+ # config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
114
+
115
+ # Series of attempts to detect a default value method for collection.
116
+ # config.collection_value_methods = [ :id, :to_s ]
117
+
118
+ # You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
119
+ # config.collection_wrapper_tag = nil
120
+
121
+ # You can define the class to use on all collection wrappers. Defaulting to none.
122
+ # config.collection_wrapper_class = nil
123
+
124
+ # You can wrap each item in a collection of radio/check boxes with a tag,
125
+ # defaulting to :span. Please note that when using :boolean_style = :nested,
126
+ # SimpleForm will force this option to be a label.
127
+ # config.item_wrapper_tag = :span
128
+
129
+ # You can define a class to use in all item wrappers. Defaulting to none.
130
+ # config.item_wrapper_class = nil
131
+
132
+ # How the label text should be generated altogether with the required text.
133
+ # config.label_text = lambda { |label, required| "#{required} #{label}" }
134
+
135
+ # You can define the class to use on all labels. Default is nil.
136
+ config.label_class = 'control-label'
137
+
138
+ # You can define the class to use on all forms. Default is simple_form.
139
+ # config.form_class = :simple_form
140
+
141
+ # You can define which elements should obtain additional classes
142
+ # config.generate_additional_classes_for = [:wrapper, :label, :input]
143
+
144
+ # Whether attributes are required by default (or not). Default is true.
145
+ # config.required_by_default = true
146
+
147
+ # Tell browsers whether to use default HTML5 validations (novalidate option).
148
+ # Default is enabled.
149
+ config.browser_validations = false
150
+
151
+ # Collection of methods to detect if a file type was given.
152
+ # config.file_methods = [ :mounted_as, :file?, :public_filename ]
153
+
154
+ # Custom mappings for input types. This should be a hash containing a regexp
155
+ # to match as key, and the input type that will be used when the field name
156
+ # matches the regexp as value.
157
+ # config.input_mappings = { /count/ => :integer }
158
+
159
+ # Default priority for time_zone inputs.
160
+ # config.time_zone_priority = nil
161
+
162
+ # Default priority for country inputs.
163
+ # config.country_priority = nil
164
+
165
+ # Default size for text inputs.
166
+ # config.default_input_size = 50
167
+
168
+ # When false, do not use translations for labels.
169
+ # config.translate_labels = true
170
+
171
+ # Automatically discover new inputs in Rails' autoload path.
172
+ # config.inputs_discovery = true
173
+
174
+ # Cache SimpleForm inputs discovery
175
+ # config.cache_discovery = !Rails.env.development?
176
+ end
@@ -1,9 +1,46 @@
1
1
  module SimpleForm
2
2
  module ActionViewExtensions
3
+ # Base builder to handle each instance of a collection of radio buttons / check boxes.
4
+ # Based on (at this time upcoming) Rails 4 collection builders.
5
+ class BuilderBase #:nodoc:
6
+ attr_reader :object, :text, :value
7
+
8
+ def initialize(template_object, object_name, method_name, object,
9
+ sanitized_attribute_name, text, value, input_html_options)
10
+ @template_object = template_object
11
+ @object_name = object_name
12
+ @method_name = method_name
13
+ @object = object
14
+ @sanitized_attribute_name = sanitized_attribute_name
15
+ @text = text
16
+ @value = value
17
+ @input_html_options = input_html_options
18
+ end
19
+
20
+ def label(label_html_options={}, &block)
21
+ @template_object.label(@object_name, @sanitized_attribute_name, @text, label_html_options, &block)
22
+ end
23
+ end
24
+
25
+ # Handles generating an instance of radio + label for collection_radio_buttons.
26
+ class RadioButtonBuilder < BuilderBase #:nodoc:
27
+ def radio_button(extra_html_options={})
28
+ html_options = extra_html_options.merge(@input_html_options)
29
+ @template_object.radio_button(@object_name, @method_name, @value, html_options)
30
+ end
31
+ end
32
+
33
+ # Handles generating an instance of check box + label for collection_check_boxes.
34
+ class CheckBoxBuilder < BuilderBase #:nodoc:
35
+ def check_box(extra_html_options={})
36
+ html_options = extra_html_options.merge(@input_html_options)
37
+ @template_object.check_box(@object_name, @method_name, html_options, @value, nil)
38
+ end
39
+ end
40
+
3
41
  # A collection of methods required by simple_form but added to rails default form.
4
42
  # This means that you can use such methods outside simple_form context.
5
43
  module Builder
6
-
7
44
  # Create a collection of radio inputs for the attribute. Basically this
8
45
  # helper will create a radio input associated with a label for each
9
46
  # text/value option in the collection, using value_method and text_method
@@ -14,13 +51,24 @@ module SimpleForm
14
51
  # == Examples
15
52
  #
16
53
  # form_for @user do |f|
17
- # f.collection_radio :options, [[true, 'Yes'] ,[false, 'No']], :first, :last
54
+ # f.collection_radio_buttons :options, [[true, 'Yes'] ,[false, 'No']], :first, :last
18
55
  # end
19
56
  #
20
57
  # <input id="user_options_true" name="user[options]" type="radio" value="true" />
21
- # <label class="collection_radio" for="user_options_true">Yes</label>
58
+ # <label class="collection_radio_buttons" for="user_options_true">Yes</label>
22
59
  # <input id="user_options_false" name="user[options]" type="radio" value="false" />
23
- # <label class="collection_radio" for="user_options_false">No</label>
60
+ # <label class="collection_radio_buttons" for="user_options_false">No</label>
61
+ #
62
+ # It is also possible to give a block that should generate the radio +
63
+ # label. To wrap the radio with the label, for instance:
64
+ #
65
+ # form_for @user do |f|
66
+ # f.collection_radio_buttons(
67
+ # :options, [[true, 'Yes'] ,[false, 'No']], :first, :last
68
+ # ) do |b|
69
+ # b.label { b.radio_button + b.text }
70
+ # end
71
+ # end
24
72
  #
25
73
  # == Options
26
74
  #
@@ -31,17 +79,37 @@ module SimpleForm
31
79
  # * disabled => the value or values that should be disabled. Accepts a single
32
80
  # item or an array of items.
33
81
  #
34
- # * collection_wrapper_tag => the tag to wrap the entire collection.
82
+ # * collection_wrapper_tag => the tag to wrap the entire collection.
83
+ #
84
+ # * collection_wrapper_class => the CSS class to use for collection_wrapper_tag
35
85
  #
36
- # * item_wrapper_tag => the tag to wrap each item in the collection.
86
+ # * item_wrapper_tag => the tag to wrap each item in the collection.
37
87
  #
38
- def collection_radio(attribute, collection, value_method, text_method, options={}, html_options={})
39
- render_collection(
40
- attribute, collection, value_method, text_method, options, html_options
41
- ) do |value, text, default_html_options|
42
- radio_button(attribute, value, default_html_options) +
43
- label(sanitize_attribute_name(attribute, value), text, :class => "collection_radio")
88
+ # * item_wrapper_class => the CSS class to use for item_wrapper_tag
89
+ #
90
+ # * a block => to generate the label + radio or any other component.
91
+ #
92
+ def collection_radio_buttons(attribute, collection, value_method, text_method, options={}, html_options={})
93
+ rendered_collection = render_collection(
94
+ collection, value_method, text_method, options, html_options
95
+ ) do |item, value, text, default_html_options|
96
+ builder = instantiate_builder(RadioButtonBuilder, attribute, item, value, text, default_html_options)
97
+
98
+ if block_given?
99
+ yield builder
100
+ else
101
+ builder.radio_button + builder.label(:class => "collection_radio_buttons")
102
+ end
44
103
  end
104
+
105
+ wrap_rendered_collection(rendered_collection, options)
106
+ end
107
+
108
+ # deprecated
109
+ def collection_radio(*args, &block)
110
+ SimpleForm.deprecation_warn "The `collection_radio` helper is deprecated, " \
111
+ "please use `collection_radio_buttons` instead."
112
+ collection_radio_buttons(*args, &block)
45
113
  end
46
114
 
47
115
  # Creates a collection of check boxes for each item in the collection,
@@ -63,32 +131,59 @@ module SimpleForm
63
131
  # <input id="user_options_false" name="user[options][]" type="checkbox" value="false" />
64
132
  # <label class="collection_check_boxes" for="user_options_false">No</label>
65
133
  #
134
+ # It is also possible to give a block that should generate the check box +
135
+ # label. To wrap the check box with the label, for instance:
136
+ #
137
+ # form_for @user do |f|
138
+ # f.collection_check_boxes(
139
+ # :options, [[true, 'Yes'] ,[false, 'No']], :first, :last
140
+ # ) do |b|
141
+ # b.label { b.check_box + b.text }
142
+ # end
143
+ # end
144
+ #
66
145
  # == Options
67
146
  #
68
147
  # Collection check box accepts some extra options:
69
148
  #
70
149
  # * checked => the value or values that should be checked initially. Accepts
71
- # a single item or an array of items.
150
+ # a single item or an array of items. It overrides existing associations.
72
151
  #
73
152
  # * disabled => the value or values that should be disabled. Accepts a single
74
153
  # item or an array of items.
75
154
  #
76
- # * collection_wrapper_tag => the tag to wrap the entire collection.
155
+ # * collection_wrapper_tag => the tag to wrap the entire collection.
156
+ #
157
+ # * collection_wrapper_class => the CSS class to use for collection_wrapper_tag
158
+ #
159
+ # * item_wrapper_tag => the tag to wrap each item in the collection.
77
160
  #
78
- # * item_wrapper_tag => the tag to wrap each item in the collection.
161
+ # * item_wrapper_class => the CSS class to use for item_wrapper_tag
162
+ #
163
+ # * a block => to generate the label + check box or any other component.
79
164
  #
80
165
  def collection_check_boxes(attribute, collection, value_method, text_method, options={}, html_options={})
81
- render_collection(
82
- attribute, collection, value_method, text_method, options, html_options
83
- ) do |value, text, default_html_options|
166
+ rendered_collection = render_collection(
167
+ collection, value_method, text_method, options, html_options
168
+ ) do |item, value, text, default_html_options|
84
169
  default_html_options[:multiple] = true
170
+ builder = instantiate_builder(CheckBoxBuilder, attribute, item, value, text, default_html_options)
85
171
 
86
- check_box(attribute, default_html_options, value, '') +
87
- label(sanitize_attribute_name(attribute, value), text, :class => "collection_check_boxes")
172
+ if block_given?
173
+ yield builder
174
+ else
175
+ builder.check_box + builder.label(:class => "collection_check_boxes")
176
+ end
88
177
  end
178
+
179
+ # Append a hidden field to make sure something will be sent back to the
180
+ # server if all checkboxes are unchecked.
181
+ hidden = template.hidden_field_tag("#{object_name}[#{attribute}][]", "", :id => nil)
182
+
183
+ wrap_rendered_collection(rendered_collection + hidden, options)
89
184
  end
90
185
 
91
- # Wrapper for using simple form inside a default rails form.
186
+ # Wrapper for using SimpleForm inside a default rails form.
92
187
  # Example:
93
188
  #
94
189
  # form_for @user do |f|
@@ -99,6 +194,8 @@ module SimpleForm
99
194
  # end
100
195
  def simple_fields_for(*args, &block)
101
196
  options = args.extract_options!
197
+ options[:wrapper] ||= self.options[:wrapper]
198
+
102
199
  if self.class < ActionView::Helpers::FormBuilder
103
200
  options[:builder] ||= self.class
104
201
  else
@@ -107,7 +204,12 @@ module SimpleForm
107
204
  fields_for(*(args << options), &block)
108
205
  end
109
206
 
110
- private
207
+ private
208
+
209
+ def instantiate_builder(builder_class, attribute, item, value, text, html_options)
210
+ builder_class.new(@template, object_name, attribute, item,
211
+ sanitize_attribute_name(attribute, value), text, value, html_options)
212
+ end
111
213
 
112
214
  # Generate default options for collection helpers, such as :checked and
113
215
  # :disabled.
@@ -115,77 +217,122 @@ module SimpleForm
115
217
  html_options = html_options.dup
116
218
 
117
219
  [:checked, :selected, :disabled].each do |option|
118
- next unless options[option]
220
+ next unless current_option = options[option]
119
221
 
120
- accept = if options[option].respond_to?(:call)
121
- options[option].call(item)
222
+ accept = if current_option.respond_to?(:call)
223
+ current_option.call(item)
122
224
  else
123
- Array(options[option]).include?(value)
225
+ Array(current_option).include?(value)
124
226
  end
125
227
 
126
- html_options[option] = true if accept
228
+ if accept
229
+ html_options[option] = true
230
+ elsif option == :checked
231
+ html_options[option] = false
232
+ end
127
233
  end
128
234
 
129
235
  html_options
130
236
  end
131
237
 
132
- def sanitize_attribute_name(attribute, value)
238
+ def sanitize_attribute_name(attribute, value) #:nodoc:
133
239
  "#{attribute}_#{value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase}"
134
240
  end
135
241
 
136
- def render_collection(attribute, collection, value_method, text_method, options={}, html_options={}) #:nodoc:
137
- collection_wrapper_tag = options.has_key?(:collection_wrapper_tag) ? options[:collection_wrapper_tag] : SimpleForm.collection_wrapper_tag
138
- item_wrapper_tag = options.has_key?(:item_wrapper_tag) ? options[:item_wrapper_tag] : SimpleForm.item_wrapper_tag
242
+ def render_collection(collection, value_method, text_method, options={}, html_options={}) #:nodoc:
243
+ item_wrapper_tag = options.fetch(:item_wrapper_tag, :span)
244
+ item_wrapper_class = options[:item_wrapper_class]
139
245
 
140
- rendered_collection = collection.map do |item|
246
+ collection.map do |item|
141
247
  value = value_for_collection(item, value_method)
142
248
  text = value_for_collection(item, text_method)
143
249
  default_html_options = default_html_options_for_collection(item, value, options, html_options)
144
250
 
145
- rendered_item = yield value, text, default_html_options
251
+ rendered_item = yield item, value, text, default_html_options
146
252
 
147
- item_wrapper_tag ? @template.content_tag(item_wrapper_tag, rendered_item) : rendered_item
253
+ item_wrapper_tag ? @template.content_tag(item_wrapper_tag, rendered_item, :class => item_wrapper_class) : rendered_item
148
254
  end.join.html_safe
149
-
150
- collection_wrapper_tag ? @template.content_tag(collection_wrapper_tag, rendered_collection) : rendered_collection
151
255
  end
152
256
 
153
257
  def value_for_collection(item, value) #:nodoc:
154
258
  value.respond_to?(:call) ? value.call(item) : item.send(value)
155
259
  end
260
+
261
+ def wrap_rendered_collection(collection, options)
262
+ wrapper_tag = options[:collection_wrapper_tag]
263
+
264
+ if wrapper_tag
265
+ wrapper_class = options[:collection_wrapper_class]
266
+ @template.content_tag(wrapper_tag, collection, :class => wrapper_class)
267
+ else
268
+ collection
269
+ end
270
+ end
156
271
  end
157
272
  end
158
273
  end
159
274
 
160
- class ActionView::Helpers::FormBuilder
161
- include SimpleForm::ActionViewExtensions::Builder
275
+ module ActionView::Helpers
276
+ class FormBuilder
277
+ include SimpleForm::ActionViewExtensions::Builder
162
278
 
163
- # Override default Rails collection_select helper to handle lambdas/procs in
164
- # text and value methods, so it works the same way as collection_radio and
165
- # collection_check_boxes in SimpleForm. If none of text/value methods is a
166
- # callable object, then it just delegates back to original collection select.
167
- #
168
- alias :original_collection_select :collection_select
169
- def collection_select(attribute, collection, value_method, text_method, options={}, html_options={})
170
- if value_method.respond_to?(:call) || text_method.respond_to?(:call)
171
- collection = collection.map do |item|
172
- value = value_for_collection(item, value_method)
173
- text = value_for_collection(item, text_method)
279
+ # Override default Rails collection_select helper to handle lambdas/procs in
280
+ # text and value methods, so it works the same way as collection_radio_buttons
281
+ # and collection_check_boxes in SimpleForm. If none of text/value methods is a
282
+ # callable object, then it just delegates back to original collection select.
283
+ #
284
+ alias :original_collection_select :collection_select
285
+ def collection_select(attribute, collection, value_method, text_method, options={}, html_options={})
286
+ if value_method.respond_to?(:call) || text_method.respond_to?(:call)
287
+ collection = collection.map do |item|
288
+ value = value_for_collection(item, value_method)
289
+ text = value_for_collection(item, text_method)
174
290
 
175
- default_html_options = default_html_options_for_collection(item, value, options, html_options)
176
- disabled = value if default_html_options[:disabled]
177
- selected = value if default_html_options[:selected]
291
+ default_html_options = default_html_options_for_collection(item, value, options, html_options)
292
+ disabled = value if default_html_options[:disabled]
293
+ selected = value if default_html_options[:selected]
178
294
 
179
- [value, text, selected, disabled]
180
- end
295
+ [value, text, selected, disabled]
296
+ end
181
297
 
182
- [:disabled, :selected].each do |option|
183
- option_value = collection.map(&:pop).compact
184
- options[option] = option_value if option_value.present?
298
+ [:disabled, :selected].each do |option|
299
+ option_value = collection.map(&:pop).compact
300
+ options[option] = option_value if option_value.present?
301
+ end
302
+ value_method, text_method = :first, :last
185
303
  end
186
- value_method, text_method = :first, :last
304
+
305
+ original_collection_select(attribute, collection, value_method, text_method, options, html_options)
187
306
  end
307
+ end
188
308
 
189
- original_collection_select(attribute, collection, value_method, text_method, options, html_options)
309
+ # Backport Rails fix to checkbox tag element, which does not generate the
310
+ # hidden input when given nil as unchecked value. This is to make SimpleForm
311
+ # collection check boxes helper to work fine with nested boolean style, when
312
+ # they are wrapped in labels. Without that, clicking in the label would
313
+ # actually change the hidden input, instead of the checkbox.
314
+ # FIXME: remove when support only Rails >= 3.2.2.
315
+ class InstanceTag
316
+ def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
317
+ options = options.stringify_keys
318
+ options["type"] = "checkbox"
319
+ options["value"] = checked_value
320
+ if options.has_key?("checked")
321
+ cv = options.delete "checked"
322
+ checked = cv == true || cv == "checked"
323
+ else
324
+ checked = self.class.check_box_checked?(value(object), checked_value)
325
+ end
326
+ options["checked"] = "checked" if checked
327
+ if options["multiple"]
328
+ add_default_name_and_id_for_value(checked_value, options)
329
+ options.delete("multiple")
330
+ else
331
+ add_default_name_and_id(options)
332
+ end
333
+ hidden = unchecked_value ? tag("input", "name" => options["name"], "type" => "hidden", "value" => unchecked_value, "disabled" => options["disabled"]) : "".html_safe
334
+ checkbox = tag("input", options)
335
+ hidden + checkbox
336
+ end
190
337
  end
191
338
  end