formtastic 1.2.4 → 3.1.5

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 (189) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +46 -0
  5. data/.yardopts +1 -0
  6. data/Appraisals +43 -0
  7. data/CHANGELOG +54 -0
  8. data/DEPRECATIONS +52 -0
  9. data/Gemfile +3 -0
  10. data/README.md +629 -0
  11. data/RELEASE_PROCESS +6 -0
  12. data/Rakefile +35 -0
  13. data/app/assets/stylesheets/formtastic.css +289 -0
  14. data/app/assets/stylesheets/formtastic_ie6.css +33 -0
  15. data/app/assets/stylesheets/formtastic_ie7.css +23 -0
  16. data/formtastic.gemspec +42 -0
  17. data/gemfiles/rails_3.2.gemfile +9 -0
  18. data/gemfiles/rails_4.0.4.gemfile +8 -0
  19. data/gemfiles/rails_4.1.gemfile +8 -0
  20. data/gemfiles/rails_4.2.gemfile +8 -0
  21. data/gemfiles/rails_4.gemfile +8 -0
  22. data/gemfiles/rails_5.0.gemfile +8 -0
  23. data/gemfiles/rails_edge.gemfile +15 -0
  24. data/lib/formtastic.rb +40 -1945
  25. data/lib/formtastic/action_class_finder.rb +18 -0
  26. data/lib/formtastic/actions.rb +11 -0
  27. data/lib/formtastic/actions/base.rb +156 -0
  28. data/lib/formtastic/actions/button_action.rb +67 -0
  29. data/lib/formtastic/actions/buttonish.rb +17 -0
  30. data/lib/formtastic/actions/input_action.rb +70 -0
  31. data/lib/formtastic/actions/link_action.rb +88 -0
  32. data/lib/formtastic/deprecation.rb +42 -0
  33. data/lib/formtastic/engine.rb +11 -0
  34. data/lib/formtastic/form_builder.rb +124 -0
  35. data/lib/formtastic/helpers.rb +16 -0
  36. data/lib/formtastic/helpers/action_helper.rb +162 -0
  37. data/lib/formtastic/helpers/actions_helper.rb +168 -0
  38. data/lib/formtastic/helpers/enum.rb +13 -0
  39. data/lib/formtastic/helpers/errors_helper.rb +81 -0
  40. data/lib/formtastic/helpers/fieldset_wrapper.rb +80 -0
  41. data/lib/formtastic/helpers/file_column_detection.rb +16 -0
  42. data/lib/formtastic/helpers/form_helper.rb +203 -0
  43. data/lib/formtastic/helpers/input_helper.rb +407 -0
  44. data/lib/formtastic/helpers/inputs_helper.rb +411 -0
  45. data/lib/formtastic/helpers/reflection.rb +37 -0
  46. data/lib/formtastic/html_attributes.rb +32 -0
  47. data/lib/formtastic/i18n.rb +4 -2
  48. data/lib/formtastic/input_class_finder.rb +18 -0
  49. data/lib/formtastic/inputs.rb +39 -0
  50. data/lib/formtastic/inputs/base.rb +76 -0
  51. data/lib/formtastic/inputs/base/associations.rb +31 -0
  52. data/lib/formtastic/inputs/base/choices.rb +108 -0
  53. data/lib/formtastic/inputs/base/collections.rb +159 -0
  54. data/lib/formtastic/inputs/base/database.rb +22 -0
  55. data/lib/formtastic/inputs/base/datetime_pickerish.rb +85 -0
  56. data/lib/formtastic/inputs/base/errors.rb +58 -0
  57. data/lib/formtastic/inputs/base/fileish.rb +23 -0
  58. data/lib/formtastic/inputs/base/hints.rb +31 -0
  59. data/lib/formtastic/inputs/base/html.rb +53 -0
  60. data/lib/formtastic/inputs/base/labelling.rb +52 -0
  61. data/lib/formtastic/inputs/base/naming.rb +42 -0
  62. data/lib/formtastic/inputs/base/numeric.rb +50 -0
  63. data/lib/formtastic/inputs/base/options.rb +17 -0
  64. data/lib/formtastic/inputs/base/placeholder.rb +17 -0
  65. data/lib/formtastic/inputs/base/stringish.rb +38 -0
  66. data/lib/formtastic/inputs/base/timeish.rb +241 -0
  67. data/lib/formtastic/inputs/base/validations.rb +215 -0
  68. data/lib/formtastic/inputs/base/wrapping.rb +50 -0
  69. data/lib/formtastic/inputs/boolean_input.rb +118 -0
  70. data/lib/formtastic/inputs/check_boxes_input.rb +197 -0
  71. data/lib/formtastic/inputs/color_input.rb +42 -0
  72. data/lib/formtastic/inputs/country_input.rb +86 -0
  73. data/lib/formtastic/inputs/datalist_input.rb +41 -0
  74. data/lib/formtastic/inputs/date_picker_input.rb +93 -0
  75. data/lib/formtastic/inputs/date_select_input.rb +34 -0
  76. data/lib/formtastic/inputs/datetime_picker_input.rb +103 -0
  77. data/lib/formtastic/inputs/datetime_select_input.rb +12 -0
  78. data/lib/formtastic/inputs/email_input.rb +41 -0
  79. data/lib/formtastic/inputs/file_input.rb +42 -0
  80. data/lib/formtastic/inputs/hidden_input.rb +62 -0
  81. data/lib/formtastic/inputs/number_input.rb +88 -0
  82. data/lib/formtastic/inputs/password_input.rb +41 -0
  83. data/lib/formtastic/inputs/phone_input.rb +42 -0
  84. data/lib/formtastic/inputs/radio_input.rb +163 -0
  85. data/lib/formtastic/inputs/range_input.rb +95 -0
  86. data/lib/formtastic/inputs/search_input.rb +41 -0
  87. data/lib/formtastic/inputs/select_input.rb +235 -0
  88. data/lib/formtastic/inputs/string_input.rb +36 -0
  89. data/lib/formtastic/inputs/text_input.rb +48 -0
  90. data/lib/formtastic/inputs/time_picker_input.rb +99 -0
  91. data/lib/formtastic/inputs/time_select_input.rb +38 -0
  92. data/lib/formtastic/inputs/time_zone_input.rb +58 -0
  93. data/lib/formtastic/inputs/url_input.rb +41 -0
  94. data/lib/formtastic/localized_string.rb +17 -0
  95. data/lib/formtastic/localizer.rb +152 -0
  96. data/lib/formtastic/namespaced_class_finder.rb +99 -0
  97. data/lib/formtastic/util.rb +35 -16
  98. data/lib/formtastic/version.rb +3 -0
  99. data/lib/generators/formtastic/form/form_generator.rb +64 -37
  100. data/lib/generators/formtastic/input/input_generator.rb +46 -0
  101. data/lib/generators/formtastic/install/install_generator.rb +13 -5
  102. data/lib/generators/templates/_form.html.erb +10 -4
  103. data/lib/generators/templates/_form.html.haml +8 -4
  104. data/lib/generators/templates/_form.html.slim +8 -0
  105. data/lib/generators/templates/formtastic.rb +77 -44
  106. data/lib/generators/templates/input.rb +19 -0
  107. data/lib/locale/en.yml +3 -0
  108. data/sample/basic_inputs.html +224 -0
  109. data/sample/config.ru +69 -0
  110. data/sample/index.html +14 -0
  111. data/spec/action_class_finder_spec.rb +12 -0
  112. data/spec/actions/button_action_spec.rb +63 -0
  113. data/spec/actions/generic_action_spec.rb +521 -0
  114. data/spec/actions/input_action_spec.rb +59 -0
  115. data/spec/actions/link_action_spec.rb +92 -0
  116. data/spec/builder/custom_builder_spec.rb +116 -0
  117. data/spec/builder/error_proc_spec.rb +27 -0
  118. data/spec/builder/semantic_fields_for_spec.rb +142 -0
  119. data/spec/fast_spec_helper.rb +12 -0
  120. data/spec/generators/formtastic/form/form_generator_spec.rb +131 -0
  121. data/spec/generators/formtastic/input/input_generator_spec.rb +124 -0
  122. data/spec/generators/formtastic/install/install_generator_spec.rb +47 -0
  123. data/spec/helpers/action_helper_spec.rb +19 -0
  124. data/spec/helpers/actions_helper_spec.rb +143 -0
  125. data/spec/helpers/form_helper_spec.rb +218 -0
  126. data/spec/helpers/input_helper_spec.rb +6 -0
  127. data/spec/helpers/inputs_helper_spec.rb +655 -0
  128. data/spec/helpers/namespaced_action_helper_spec.rb +43 -0
  129. data/spec/helpers/namespaced_input_helper_spec.rb +36 -0
  130. data/spec/helpers/reflection_helper_spec.rb +32 -0
  131. data/spec/helpers/semantic_errors_helper_spec.rb +112 -0
  132. data/spec/i18n_spec.rb +210 -0
  133. data/spec/input_class_finder_spec.rb +10 -0
  134. data/spec/inputs/base/collections_spec.rb +76 -0
  135. data/spec/inputs/base/validations_spec.rb +342 -0
  136. data/spec/inputs/boolean_input_spec.rb +254 -0
  137. data/spec/inputs/check_boxes_input_spec.rb +546 -0
  138. data/spec/inputs/color_input_spec.rb +97 -0
  139. data/spec/inputs/country_input_spec.rb +133 -0
  140. data/spec/inputs/custom_input_spec.rb +55 -0
  141. data/spec/inputs/datalist_input_spec.rb +61 -0
  142. data/spec/inputs/date_picker_input_spec.rb +449 -0
  143. data/spec/inputs/date_select_input_spec.rb +235 -0
  144. data/spec/inputs/datetime_picker_input_spec.rb +490 -0
  145. data/spec/inputs/datetime_select_input_spec.rb +193 -0
  146. data/spec/inputs/email_input_spec.rb +85 -0
  147. data/spec/inputs/file_input_spec.rb +89 -0
  148. data/spec/inputs/hidden_input_spec.rb +135 -0
  149. data/spec/inputs/include_blank_spec.rb +78 -0
  150. data/spec/inputs/label_spec.rb +149 -0
  151. data/spec/inputs/number_input_spec.rb +815 -0
  152. data/spec/inputs/password_input_spec.rb +99 -0
  153. data/spec/inputs/phone_input_spec.rb +85 -0
  154. data/spec/inputs/placeholder_spec.rb +71 -0
  155. data/spec/inputs/radio_input_spec.rb +328 -0
  156. data/spec/inputs/range_input_spec.rb +505 -0
  157. data/spec/inputs/readonly_spec.rb +50 -0
  158. data/spec/inputs/search_input_spec.rb +84 -0
  159. data/spec/inputs/select_input_spec.rb +615 -0
  160. data/spec/inputs/string_input_spec.rb +260 -0
  161. data/spec/inputs/text_input_spec.rb +187 -0
  162. data/spec/inputs/time_picker_input_spec.rb +455 -0
  163. data/spec/inputs/time_select_input_spec.rb +248 -0
  164. data/spec/inputs/time_zone_input_spec.rb +143 -0
  165. data/spec/inputs/url_input_spec.rb +85 -0
  166. data/spec/inputs/with_options_spec.rb +43 -0
  167. data/spec/localizer_spec.rb +130 -0
  168. data/spec/namespaced_class_finder_spec.rb +79 -0
  169. data/spec/spec.opts +2 -0
  170. data/spec/spec_helper.rb +525 -0
  171. data/spec/support/custom_macros.rb +564 -0
  172. data/spec/support/deprecation.rb +6 -0
  173. data/spec/support/shared_examples.rb +1313 -0
  174. data/spec/support/specialized_class_finder_shared_example.rb +27 -0
  175. data/spec/support/test_environment.rb +31 -0
  176. data/spec/util_spec.rb +66 -0
  177. metadata +434 -161
  178. data/README.textile +0 -682
  179. data/generators/form/USAGE +0 -16
  180. data/generators/form/form_generator.rb +0 -111
  181. data/generators/formtastic/formtastic_generator.rb +0 -26
  182. data/init.rb +0 -5
  183. data/lib/formtastic/layout_helper.rb +0 -12
  184. data/lib/formtastic/railtie.rb +0 -14
  185. data/lib/generators/templates/formtastic.css +0 -145
  186. data/lib/generators/templates/formtastic_changes.css +0 -14
  187. data/lib/generators/templates/rails2/_form.html.erb +0 -5
  188. data/lib/generators/templates/rails2/_form.html.haml +0 -4
  189. data/rails/init.rb +0 -2
@@ -0,0 +1,13 @@
1
+ module Formtastic
2
+ module Helpers
3
+ # @private
4
+ module Enum
5
+ # Returns the enum (if defined) for the given method
6
+ def enum_for(method) # @private
7
+ if @object.respond_to?(:defined_enums)
8
+ @object.defined_enums[method.to_s]
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,81 @@
1
+ module Formtastic
2
+ module Helpers
3
+ module ErrorsHelper
4
+ include Formtastic::Helpers::FileColumnDetection
5
+ include Formtastic::Helpers::Reflection
6
+ include Formtastic::LocalizedString
7
+
8
+ INLINE_ERROR_TYPES = [:sentence, :list, :first]
9
+
10
+ # Generates an unordered list of error messages on the base object and optionally for a given
11
+ # set of named attribute. This is idea for rendering a block of error messages at the top of
12
+ # the form for hidden/special/virtual attributes (the Paperclip Rails plugin does this), or
13
+ # errors on the base model.
14
+ #
15
+ # A hash can be used as the last set of arguments to pass HTML attributes to the `<ul>`
16
+ # wrapper.
17
+ #
18
+ # @example A list of errors on the base model
19
+ # <%= semantic_form_for ... %>
20
+ # <%= f.semantic_errors %>
21
+ # ...
22
+ # <% end %>
23
+ #
24
+ # @example A list of errors on the base and named attributes
25
+ # <%= semantic_form_for ... %>
26
+ # <%= f.semantic_errors :something_special %>
27
+ # ...
28
+ # <% end %>
29
+ #
30
+ # @example A list of errors on the base model, with custom HTML attributes
31
+ # <%= semantic_form_for ... %>
32
+ # <%= f.semantic_errors :class => "awesome" %>
33
+ # ...
34
+ # <% end %>
35
+ #
36
+ # @example A list of errors on the base model and named attributes, with custom HTML attributes
37
+ # <%= semantic_form_for ... %>
38
+ # <%= f.semantic_errors :something_special, :something_else, :class => "awesome", :onclick => "Awesome();" %>
39
+ # ...
40
+ # <% end %>
41
+ def semantic_errors(*args)
42
+ html_options = args.extract_options!
43
+ args = args - [:base]
44
+ full_errors = args.inject([]) do |array, method|
45
+ attribute = localized_string(method, method.to_sym, :label) || humanized_attribute_name(method)
46
+ errors = Array(@object.errors[method.to_sym]).to_sentence
47
+ errors.present? ? array << [attribute, errors].join(" ") : array ||= []
48
+ end
49
+ full_errors << @object.errors[:base]
50
+ full_errors.flatten!
51
+ full_errors.compact!
52
+ return nil if full_errors.blank?
53
+ html_options[:class] ||= "errors"
54
+ template.content_tag(:ul, html_options) do
55
+ Formtastic::Util.html_safe(full_errors.map { |error| template.content_tag(:li, Formtastic::Util.html_safe(error)) }.join)
56
+ end
57
+ end
58
+
59
+ protected
60
+
61
+ def error_keys(method, options)
62
+ @methods_for_error ||= {}
63
+ @methods_for_error[method] ||= begin
64
+ methods_for_error = [method.to_sym]
65
+ methods_for_error << file_metadata_suffixes.map{|suffix| "#{method}_#{suffix}".to_sym} if is_file?(method, options)
66
+ methods_for_error << [association_primary_key_for_method(method)] if [:belongs_to, :has_many].include? association_macro_for_method(method)
67
+ methods_for_error.flatten.compact.uniq
68
+ end
69
+ end
70
+
71
+ def has_errors?(method, options)
72
+ methods_for_error = error_keys(method,options)
73
+ @object && @object.respond_to?(:errors) && methods_for_error.any?{|error| !@object.errors[error].blank?}
74
+ end
75
+
76
+ def render_inline_errors?
77
+ @object && @object.respond_to?(:errors) && Formtastic::FormBuilder::INLINE_ERROR_TYPES.include?(inline_errors)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,80 @@
1
+ module Formtastic
2
+ module Helpers
3
+ # @private
4
+ module FieldsetWrapper
5
+
6
+ protected
7
+
8
+ # Generates a fieldset and wraps the content in an ordered list. When working
9
+ # with nested attributes, it allows %i as interpolation option in :name. So you can do:
10
+ #
11
+ # f.inputs :name => 'Task #%i', :for => :tasks
12
+ #
13
+ # or the shorter equivalent:
14
+ #
15
+ # f.inputs 'Task #%i', :for => :tasks
16
+ #
17
+ # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
18
+ # 'Task #3' and so on.
19
+ #
20
+ # Note: Special case for the inline inputs (non-block):
21
+ # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
22
+ # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
23
+ # f.inputs :title, :body, :author # First argument is a column => (no legend)
24
+ def field_set_and_list_wrapping(*args, &block) # @private
25
+ contents = args[-1].is_a?(::Hash) ? '' : args.pop.flatten
26
+ html_options = args.extract_options!
27
+
28
+ if block_given?
29
+ contents = if template.respond_to?(:is_haml?) && template.is_haml?
30
+ template.capture_haml(&block)
31
+ else
32
+ template.capture(&block)
33
+ end
34
+ end
35
+
36
+ # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
37
+ contents = contents.join if contents.respond_to?(:join)
38
+
39
+ legend = field_set_legend(html_options)
40
+ fieldset = template.content_tag(:fieldset,
41
+ Formtastic::Util.html_safe(legend) << template.content_tag(:ol, Formtastic::Util.html_safe(contents)),
42
+ html_options.except(:builder, :parent, :name)
43
+ )
44
+
45
+ fieldset
46
+ end
47
+
48
+ def field_set_legend(html_options)
49
+ legend = (html_options[:name] || '').to_s
50
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent] && legend.include?('%i') # only applying if String includes '%i' avoids argument error when $DEBUG is true
51
+ legend = template.content_tag(:legend, template.content_tag(:span, Formtastic::Util.html_safe(legend))) unless legend.blank?
52
+ legend
53
+ end
54
+
55
+ # Gets the nested_child_index value from the parent builder. It returns a hash with each
56
+ # association that the parent builds.
57
+ def parent_child_index(parent) # @private
58
+ # Could be {"post[authors_attributes]"=>0} or { :authors => 0 }
59
+ duck = parent[:builder].instance_variable_get('@nested_child_index')
60
+
61
+ # Could be symbol for the association, or a model (or an array of either, I think? TODO)
62
+ child = parent[:for]
63
+ # Pull a sybol or model out of Array (TODO: check if there's an Array)
64
+ child = child.first if child.respond_to?(:first)
65
+ # If it's an object, get a symbol from the class name
66
+ child = child.class.name.underscore.to_sym unless child.is_a?(Symbol)
67
+
68
+ key = "#{parent[:builder].object_name}[#{child}_attributes]"
69
+
70
+ # TODO: One of the tests produces a scenario where duck is "0" and the test looks for a "1"
71
+ # in the legend, so if we have a number, return it with a +1 until we can verify this scenario.
72
+ return duck + 1 if duck.is_a?(Integer)
73
+
74
+ # First try to extract key from duck Hash, then try child
75
+ (duck[key] || duck[child]).to_i + 1
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,16 @@
1
+ module Formtastic
2
+ module Helpers
3
+ # @private
4
+ module FileColumnDetection
5
+
6
+ def is_file?(method, options = {})
7
+ @files ||= {}
8
+ @files[method] ||= (options[:as].present? && options[:as] == :file) || begin
9
+ file = @object.send(method) if @object && @object.respond_to?(method)
10
+ file && file_methods.any?{|m| file.respond_to?(m)}
11
+ end
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,203 @@
1
+ module Formtastic
2
+ module Helpers
3
+
4
+ # FormHelper provides a handful of wrappers around Rails' built-in form helpers methods to set
5
+ # the `:builder` option to `Formtastic::FormBuilder` and apply some class names to the `<form>`
6
+ # tag.
7
+ #
8
+ # The following methods are wrapped:
9
+ #
10
+ # * `semantic_form_for` to `form_for`
11
+ # * `semantic_fields_for` to `fields_for`
12
+ # * `semantic_remote_form_for` and `semantic_form_remote_for` to `remote_form_for`
13
+ #
14
+ # The following two examples are effectively equivalent:
15
+ #
16
+ # <%= form_for(@post, :builder => Formtastic::FormBuilder, :class => 'formtastic post') do |f| %>
17
+ # #...
18
+ # <% end %>
19
+ #
20
+ # <%= semantic_form_for(@post) do |f| %>
21
+ # #...
22
+ # <% end %>
23
+ #
24
+ # This simple wrapping means that all arguments, options and variations supported by Rails' own
25
+ # helpers are also supported by Formtastic.
26
+ #
27
+ # Since `Formtastic::FormBuilder` subclasses Rails' own `FormBuilder`, you have access to all
28
+ # of Rails' built-in form helper methods such as `text_field`, `check_box`, `radio_button`,
29
+ # etc **in addition to** all of Formtastic's additional helpers like {InputsHelper#inputs inputs},
30
+ # {InputsHelper#input input}, {ButtonsHelper#buttons buttons}, etc:
31
+ #
32
+ # <%= semantic_form_for(@post) do |f| %>
33
+ #
34
+ # <!-- Formtastic -->
35
+ # <%= f.input :title %>
36
+ #
37
+ # <!-- Rails -->
38
+ # <li class='something-custom'>
39
+ # <%= f.label :title %>
40
+ # <%= f.text_field :title %>
41
+ # <p class='hints'>...</p>
42
+ # </li>
43
+ # <% end %>
44
+ #
45
+ # Formtastic is a superset of Rails' FormBuilder. It deliberately avoids overriding or modifying
46
+ # the behavior of Rails' own form helpers so that you can use Formtastic helpers when suited,
47
+ # and fall back to regular Rails helpers, ERB and HTML when needed. In other words, you're never
48
+ # fully committed to The Formtastic Way.
49
+ module FormHelper
50
+
51
+ # Allows the `:builder` option on `form_for` etc to be changed to your own which subclasses
52
+ # `Formtastic::FormBuilder`. Change this from `config/initializers/formtastic.rb`.
53
+ @@builder = Formtastic::FormBuilder
54
+ mattr_accessor :builder
55
+
56
+ # Allows the default class we add to all `<form>` tags to be changed from `formtastic` to
57
+ # `whatever`. Change this from `config/initializers/formtastic.rb`.
58
+ @@default_form_class = 'formtastic'
59
+ mattr_accessor :default_form_class
60
+
61
+ # Allows to set a custom proc to handle the class infered from the model's name. By default it
62
+ # will infer the name from the class name (eg. Post will be "post").
63
+ @@default_form_model_class_proc = proc { |model_class_name| model_class_name }
64
+ mattr_accessor :default_form_model_class_proc
65
+
66
+ # Allows to set a custom field_error_proc wrapper. By default this wrapper
67
+ # is disabled since `formtastic` already adds an error class to the LI tag
68
+ # containing the input. Change this from `config/initializers/formtastic.rb`.
69
+ @@formtastic_field_error_proc = proc { |html_tag, instance_tag| html_tag }
70
+ mattr_accessor :formtastic_field_error_proc
71
+
72
+ # Wrapper around Rails' own `form_for` helper to set the `:builder` option to
73
+ # `Formtastic::FormBuilder` and to set some class names on the `<form>` tag such as
74
+ # `formtastic` and the downcased and underscored model name (eg `post`).
75
+ #
76
+ # See Rails' `form_for` for full documentation of all supported arguments and options.
77
+ #
78
+ # Since `Formtastic::FormBuilder` subclasses Rails' own FormBuilder, you have access to all
79
+ # of Rails' built-in form helper methods such as `text_field`, `check_box`, `radio_button`,
80
+ # etc **in addition to** all of Formtastic's additional helpers like {InputsHelper#inputs inputs},
81
+ # {InputsHelper#input input}, {ButtonsHelper#buttons buttons}, etc.
82
+ #
83
+ # Most of the examples below have been adapted from the examples found in the Rails `form_for`
84
+ # documentation.
85
+ #
86
+ # @see http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html Rails' FormHelper documentation (`form_for`, etc)
87
+ # @see http://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html Rails' FormBuilder documentaion (`text_field`, etc)
88
+ # @see FormHelper The overview of the FormBuilder module
89
+ #
90
+ # @example Resource-oriented form generation
91
+ # <%= semantic_form_for @user do |f| %>
92
+ # <%= f.input :name %>
93
+ # <%= f.input :email %>
94
+ # <%= f.input :password %>
95
+ # <% end %>
96
+ #
97
+ # @example Generic form generation
98
+ # <%= semantic_form_for :user do |f| %>
99
+ # <%= f.input :name %>
100
+ # <%= f.input :email %>
101
+ # <%= f.input :password %>
102
+ # <% end %>
103
+ #
104
+ # @example Resource-oriented with custom URL
105
+ # <%= semantic_form_for(@post, :url => super_post_path(@post)) do |f| %>
106
+ # ...
107
+ # <% end %>
108
+ #
109
+ # @example Resource-oriented with namespaced routes
110
+ # <%= semantic_form_for([:admin, @post]) do |f| %>
111
+ # ...
112
+ # <% end %>
113
+ #
114
+ # @example Resource-oriented with nested routes
115
+ # <%= semantic_form_for([@user, @post]) do |f| %>
116
+ # ...
117
+ # <% end %>
118
+ #
119
+ # @example Rename the resource
120
+ # <%= semantic_form_for(@post, :as => :article) do |f| %>
121
+ # ...
122
+ # <% end %>
123
+ #
124
+ # @example Remote forms (unobtrusive JavaScript)
125
+ # <%= semantic_form_for(@post, :remote => true) do |f| %>
126
+ # ...
127
+ # <% end %>
128
+ #
129
+ # @example Namespaced forms all multiple Formtastic forms to exist on the one page without DOM id clashes and invalid HTML documents.
130
+ # <%= semantic_form_for(@post, :namespace => 'first') do |f| %>
131
+ # ...
132
+ # <% end %>
133
+ #
134
+ # @example Accessing a mixture of Formtastic helpers and Rails FormBuilder helpers.
135
+ # <%= semantic_form_for(@post) do |f| %>
136
+ # <%= f.input :title %>
137
+ # <%= f.input :body %>
138
+ # <li class="something-custom">
139
+ # <label><%= f.check_box :published %></label>
140
+ # </li>
141
+ # <% end %>
142
+ #
143
+ # @param record_or_name_or_array
144
+ # Same behavior as Rails' `form_for`
145
+ #
146
+ # @option *args [Hash] :html
147
+ # Pass HTML attributes into the `<form>` tag. Same behavior as Rails' `form_for`, except we add in some of our own classes.
148
+ #
149
+ # @option *args [String, Hash] :url
150
+ # A hash of URL components just like you pass into `link_to` or `url_for`, or a named route (eg `posts_path`). Same behavior as Rails' `form_for`.
151
+ #
152
+ # @option *args [String] :namespace
153
+ def semantic_form_for(record_or_name_or_array, *args, &proc)
154
+ options = args.extract_options!
155
+ options[:builder] ||= @@builder
156
+ options[:html] ||= {}
157
+ options[:html][:novalidate] = !@@builder.perform_browser_validations unless options[:html].key?(:novalidate)
158
+ options[:custom_namespace] = options.delete(:namespace)
159
+
160
+ singularizer = defined?(ActiveModel::Naming.singular) ? ActiveModel::Naming.method(:singular) : ActionController::RecordIdentifier.method(:singular_class_name)
161
+
162
+ class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
163
+ class_names << @@default_form_class
164
+ model_class_name = case record_or_name_or_array
165
+ when String, Symbol then record_or_name_or_array.to_s # :post => "post"
166
+ when Array then options[:as] || singularizer.call(record_or_name_or_array[-1].class) # [@post, @comment] # => "comment"
167
+ else options[:as] || singularizer.call(record_or_name_or_array.class) # @post => "post"
168
+ end
169
+ class_names << @@default_form_model_class_proc.call(model_class_name)
170
+
171
+ options[:html][:class] = class_names.compact.join(" ")
172
+
173
+ with_custom_field_error_proc do
174
+ self.form_for(record_or_name_or_array, *(args << options), &proc)
175
+ end
176
+ end
177
+
178
+ # Wrapper around Rails' own `fields_for` helper to set the `:builder` option to
179
+ # `Formtastic::FormBuilder`.
180
+ #
181
+ # @see #semantic_form_for
182
+ def semantic_fields_for(record_name, record_object = nil, options = {}, &block)
183
+ options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
184
+ options[:builder] ||= @@builder
185
+ options[:custom_namespace] = options.delete(:namespace)
186
+
187
+ with_custom_field_error_proc do
188
+ self.fields_for(record_name, record_object, options, &block)
189
+ end
190
+ end
191
+
192
+ protected
193
+
194
+ def with_custom_field_error_proc(&block)
195
+ default_field_error_proc = ::ActionView::Base.field_error_proc
196
+ ::ActionView::Base.field_error_proc = @@formtastic_field_error_proc
197
+ yield
198
+ ensure
199
+ ::ActionView::Base.field_error_proc = default_field_error_proc
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,407 @@
1
+ # -*- coding: utf-8 -*-
2
+ module Formtastic
3
+ module Helpers
4
+
5
+ # {#input} is used to render all content (labels, form widgets, error messages, hints, etc) for
6
+ # a single form input (or field), usually representing a single method or attribute on the
7
+ # form's object or model.
8
+ #
9
+ # The content is wrapped in an `<li>` tag, so it's usually called inside an {Formtastic::Helpers::InputsHelper#inputs inputs} block
10
+ # (which renders an `<ol>` inside a `<fieldset>`), which should be inside a {Formtastic::Helpers::FormHelper#semantic_form_for `semantic_form_for`}
11
+ # block:
12
+ #
13
+ # <%= semantic_form_for @post do |f| %>
14
+ # <%= f.inputs do %>
15
+ # <%= f.input :title %>
16
+ # <%= f.input :body %>
17
+ # <% end %>
18
+ # <% end %>
19
+ #
20
+ # The HTML output will be something like:
21
+ #
22
+ # <form class="formtastic" method="post" action="...">
23
+ # <fieldset>
24
+ # <ol>
25
+ # <li class="string required" id="post_title_input">
26
+ # ...
27
+ # </li>
28
+ # <li class="text required" id="post_body_input">
29
+ # ...
30
+ # </li>
31
+ # </ol>
32
+ # </fieldset>
33
+ # </form>
34
+ #
35
+ # @see #input
36
+ # @see Formtastic::Helpers::InputsHelper#inputs
37
+ # @see Formtastic::Helpers::FormHelper#semantic_form_for
38
+ module InputHelper
39
+ INPUT_CLASS_DEPRECATION = 'configure Formtastic::FormBuilder.input_class_finder instead (upgrade guide on wiki: http://bit.ly/1F9QtKc )'.freeze
40
+ private_constant(:INPUT_CLASS_DEPRECATION)
41
+
42
+ include Formtastic::Helpers::Reflection
43
+ include Formtastic::Helpers::Enum
44
+ include Formtastic::Helpers::FileColumnDetection
45
+
46
+ # Returns a chunk of HTML markup for a given `method` on the form object, wrapped in
47
+ # an `<li>` wrapper tag with appropriate `class` and `id` attribute hooks for CSS and JS.
48
+ # In many cases, the contents of the wrapper will be as simple as a `<label>` and an `<input>`:
49
+ #
50
+ # <%= f.input :title, :as => :string, :required => true %>
51
+ #
52
+ # <li class="string required" id="post_title_input">
53
+ # <label for="post_title">Title<abbr title="Required">*</abbr></label>
54
+ # <input type="text" name="post[title]" value="" id="post_title" required="required">
55
+ # </li>
56
+ #
57
+ # In other cases (like a series of checkboxes for a `has_many` relationship), the wrapper may
58
+ # include more complex markup, like a nested `<fieldset>` with a `<legend>` and an `<ol>` of
59
+ # checkbox/label pairs for each choice:
60
+ #
61
+ # <%= f.input :categories, :as => :check_boxes, :collection => Category.active.ordered %>
62
+ #
63
+ # <li class="check_boxes" id="post_categories_input">
64
+ # <fieldset>
65
+ # <legend>Categories</legend>
66
+ # <ol>
67
+ # <li>
68
+ # <label><input type="checkbox" name="post[categories][1]" value="1"> Ruby</label>
69
+ # </li>
70
+ # <li>
71
+ # <label><input type="checkbox" name="post[categories][2]" value="2"> Rails</label>
72
+ # </li>
73
+ # <li>
74
+ # <label><input type="checkbox" name="post[categories][2]" value="2"> Awesome</label>
75
+ # </li>
76
+ # </ol>
77
+ # </fieldset>
78
+ # </li>
79
+ #
80
+ # Sensible defaults for all options are guessed by looking at the method name, database column
81
+ # information, association information, validation information, etc. For example, a `:string`
82
+ # database column will map to a `:string` input, but if the method name contains 'email', will
83
+ # map to an `:email` input instead. `belongs_to` associations will have a `:select` input, etc.
84
+ #
85
+ # Formtastic supports many different styles of inputs, and you can/should override the default
86
+ # with the `:as` option. Internally, the symbol is used to map to a protected method
87
+ # responsible for the details. For example, `:as => :string` will map to `string_input`,
88
+ # defined in a module of the same name. Detailed documentation for each input style and it's
89
+ # supported options is available on the `*_input` method in each module (links provided below).
90
+ #
91
+ # Available input styles:
92
+ #
93
+ # * `:boolean` (see {Inputs::BooleanInput})
94
+ # * `:check_boxes` (see {Inputs::CheckBoxesInput})
95
+ # * `:color` (see {Inputs::ColorInput})
96
+ # * `:country` (see {Inputs::CountryInput})
97
+ # * `:datetime_select` (see {Inputs::DatetimeSelectInput})
98
+ # * `:date_select` (see {Inputs::DateSelectInput})
99
+ # * `:email` (see {Inputs::EmailInput})
100
+ # * `:file` (see {Inputs::FileInput})
101
+ # * `:hidden` (see {Inputs::HiddenInput})
102
+ # * `:number` (see {Inputs::NumberInput})
103
+ # * `:password` (see {Inputs::PasswordInput})
104
+ # * `:phone` (see {Inputs::PhoneInput})
105
+ # * `:radio` (see {Inputs::RadioInput})
106
+ # * `:search` (see {Inputs::SearchInput})
107
+ # * `:select` (see {Inputs::SelectInput})
108
+ # * `:string` (see {Inputs::StringInput})
109
+ # * `:text` (see {Inputs::TextInput})
110
+ # * `:time_zone` (see {Inputs::TimeZoneInput})
111
+ # * `:time_select` (see {Inputs::TimeSelectInput})
112
+ # * `:url` (see {Inputs::UrlInput})
113
+ #
114
+ # Calling `:as => :string` (for example) will call `#to_html` on a new instance of
115
+ # `Formtastic::Inputs::StringInput`. Before this, Formtastic will try to instantiate a top-level
116
+ # namespace StringInput, meaning you can subclass and modify `Formtastic::Inputs::StringInput`
117
+ # in `app/inputs/`. This also means you can create your own new input types in `app/inputs/`.
118
+ #
119
+ # @todo document the "guessing" of input style
120
+ #
121
+ # @param [Symbol] method
122
+ # The database column or method name on the form object that this input represents
123
+ #
124
+ # @option options :as [Symbol]
125
+ # Override the style of input should be rendered
126
+ #
127
+ # @option options :label [String, Symbol, false]
128
+ # Override the label text
129
+ #
130
+ # @option options :hint [String, Symbol, false]
131
+ # Override hint text
132
+ #
133
+ # @option options :required [Boolean]
134
+ # Override to mark the input as required (or not) — adds a required/optional class to the wrapper, and a HTML5 required attribute to the `<input>`
135
+ #
136
+ # @option options :input_html [Hash]
137
+ # Override or add to the HTML attributes to be passed down to the `<input>` tag
138
+ # (If you use attr_readonly method in your model, formtastic will automatically set those attributes's input readonly)
139
+ #
140
+ # @option options :wrapper_html [Hash]
141
+ # Override or add to the HTML attributes to be passed down to the wrapping `<li>` tag
142
+ #
143
+ # @option options :collection [Array<ActiveModel, String, Symbol>, Hash{String => String, Boolean}, OrderedHash{String => String, Boolean}]
144
+ # Override collection of objects in the association (`:select`, `:radio` & `:check_boxes` inputs only)
145
+ #
146
+ # @option options :multiple [Boolean]
147
+ # Specify if the `:select` input should allow multiple selections or not (defaults to `belongs_to` associations, and `true` for `has_many` and `has_and_belongs_to_many` associations)
148
+ #
149
+ # @option options :include_blank [Boolean]
150
+ # Specify if a `:select` input should include a blank option or not (defaults to `include_blank_for_select_by_default` configuration)
151
+ #
152
+ # @option options :prompt [String]
153
+ # Specify the text in the first ('blank') `:select` input `<option>` to prompt a user to make a selection (implicitly sets `:include_blank` to `true`)
154
+ #
155
+ # @todo Can we deprecate & kill `:label`, `:hint` & `:prompt`? All strings could be shifted to i18n!
156
+ #
157
+ # @example Accept all default options
158
+ # <%= f.input :title %>
159
+ #
160
+ # @example Change the input type
161
+ # <%= f.input :title, :as => :string %>
162
+ #
163
+ # @example Changing the label with a String
164
+ # <%= f.input :title, :label => "Post title" %>
165
+ #
166
+ # @example Disabling the label with false, even if an i18n translation exists
167
+ # <%= f.input :title, :label => false %>
168
+ #
169
+ # @example Changing the hint with a String
170
+ # <%= f.input :title, :hint => "Every post needs a title!" %>
171
+ #
172
+ # @example Disabling the hint with false, even if an i18n translation exists
173
+ # <%= f.input :title, :hint => false %>
174
+ #
175
+ # @example Marking a field as required or not (even if validations do not enforce it)
176
+ # <%= f.input :title, :required => true %>
177
+ # <%= f.input :title, :required => false %>
178
+ #
179
+ # @example Changing or adding to HTML attributes in the main `<input>` or `<select>` tag
180
+ # <%= f.input :title, :input_html => { :onchange => "somethingAwesome();", :class => 'awesome' } %>
181
+ #
182
+ # @example Changing or adding to HTML attributes in the wrapper `<li>` tag
183
+ # <%= f.input :title, :wrapper_html => { :class => "important-input" } %>
184
+ #
185
+ # @example Changing the association choices with `:collection`
186
+ # <%= f.input :author, :collection => User.active %>
187
+ # <%= f.input :categories, :collection => Category.where(...).order(...) %>
188
+ # <%= f.input :status, :collection => ["Draft", "Published"] %>
189
+ # <%= f.input :status, :collection => [:draft, :published] %>
190
+ # <%= f.input :status, :collection => {"Draft" => 0, "Published" => 1} %>
191
+ # <%= f.input :status, :collection => OrderedHash.new("Draft" => 0, "Published" => 1) %>
192
+ # <%= f.input :status, :collection => [["Draft", 0], ["Published", 1]] %>
193
+ # <%= f.input :status, :collection => grouped_options_for_select(...) %>
194
+ # <%= f.input :status, :collection => options_for_select(...) %>
195
+ #
196
+ # @example Specifying if a `:select` should allow multiple selections:
197
+ # <%= f.input :cateogies, :as => :select, :multiple => true %>
198
+ # <%= f.input :cateogies, :as => :select, :multiple => false %>
199
+ #
200
+ # @example Specifying if a `:select` should have a 'blank' first option to prompt selection:
201
+ # <%= f.input :author, :as => :select, :include_blank => true %>
202
+ # <%= f.input :author, :as => :select, :include_blank => false %>
203
+ #
204
+ # @example Specifying the text for a `:select` input's 'blank' first option to prompt selection:
205
+ # <%= f.input :author, :as => :select, :prompt => "Select an Author" %>
206
+ #
207
+ # @example Modifying an input to suit your needs in `app/inputs`:
208
+ # class StringInput < Formtastic::Inputs::StringInput
209
+ # def to_html
210
+ # puts "this is my custom version of StringInput"
211
+ # super
212
+ # end
213
+ # end
214
+ #
215
+ # @example Creating your own input to suit your needs in `app/inputs`:
216
+ # class DatePickerInput
217
+ # include Formtastic::Inputs::Base
218
+ # def to_html
219
+ # # ...
220
+ # end
221
+ # end
222
+ #
223
+ # @example Providing HTML5 placeholder text through i18n:
224
+ # en:
225
+ # formtastic:
226
+ # placeholders:
227
+ # user:
228
+ # email: "you@yours.com"
229
+ # first_name: "Joe"
230
+ # last_name: "Smith"
231
+ #
232
+ # @see #namespaced_input_class
233
+ # @todo Many many more examples. Some of the detail probably needs to be pushed out to the relevant methods too.
234
+ # @todo More i18n examples.
235
+ def input(method, options = {})
236
+ method = method.to_sym
237
+ options = options.dup # Allow options to be shared without being tainted by Formtastic
238
+ options[:as] ||= default_input_type(method, options)
239
+
240
+ klass = input_class(options[:as])
241
+
242
+ klass.new(self, template, @object, @object_name, method, options).to_html
243
+ end
244
+
245
+ protected
246
+
247
+ # First try if we can detect special things like :file. With CarrierWave the method does have
248
+ # an underlying column so we don't want :string to get selected.
249
+ #
250
+ # For methods that have a database column, take a best guess as to what the input method
251
+ # should be. In most cases, it will just return the column type (eg :string), but for special
252
+ # cases it will simplify (like the case of :integer, :float & :decimal to :number), or do
253
+ # something different (like :password and :select).
254
+ #
255
+ # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
256
+ # default is a :string, a similar behaviour to Rails' scaffolding.
257
+ def default_input_type(method, options = {}) # @private
258
+ if @object
259
+ return :select if reflection_for(method)
260
+
261
+ return :file if is_file?(method, options)
262
+ end
263
+
264
+ column = column_for(method)
265
+ if column && column.type
266
+ # Special cases where the column type doesn't map to an input method.
267
+ case column.type
268
+ when :string
269
+ return :password if method.to_s =~ /password/
270
+ return :country if method.to_s =~ /country$/
271
+ return :time_zone if method.to_s =~ /time_zone/
272
+ return :email if method.to_s =~ /email/
273
+ return :url if method.to_s =~ /^url$|^website$|_url$/
274
+ return :phone if method.to_s =~ /(phone|fax)/
275
+ return :search if method.to_s =~ /^search$/
276
+ return :color if method.to_s =~ /color/
277
+ when :integer
278
+ return :select if reflection_for(method)
279
+ return :select if enum_for(method)
280
+ return :number
281
+ when :float, :decimal
282
+ return :number
283
+ when :datetime, :timestamp
284
+ return :datetime_select
285
+ when :time
286
+ return :time_select
287
+ when :date
288
+ return :date_select
289
+ end
290
+
291
+ # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
292
+ return :select if column.type == :string && options.key?(:collection)
293
+ # Try 3: Assume the input name will be the same as the column type (e.g. string_input).
294
+ return column.type
295
+ else
296
+ return :select if options.key?(:collection)
297
+ return :password if method.to_s =~ /password/
298
+ return :string
299
+ end
300
+ end
301
+
302
+ # Get a column object for a specified attribute method - if possible.
303
+ def column_for(method) # @private
304
+ if @object.respond_to?(:column_for_attribute)
305
+ # Remove deprecation wrapper & review after Rails 5.0 ships
306
+ ActiveSupport::Deprecation.silence do
307
+ @object.column_for_attribute(method)
308
+ end
309
+ end
310
+ end
311
+
312
+ # Takes the `:as` option and attempts to return the corresponding input
313
+ # class. In the case of `:as => :awesome` it will first attempt to find a
314
+ # top level `AwesomeInput` class (to allow the application to subclass
315
+ # and modify to suit), falling back to `Formtastic::Inputs::AwesomeInput`.
316
+ #
317
+ # Custom input namespaces to look into can be configured via the
318
+ # {Formtastic::FormBuilder.input_namespaces} configuration setting.
319
+ #
320
+ # @param [Symbol] as A symbol representing the type of input to render
321
+ # @raise [Formtastic::UnknownInputError] An appropriate input class could not be found
322
+ # @return [Class] An input class constant
323
+ #
324
+ # @example Normal use
325
+ # input_class(:string) #=> Formtastic::Inputs::StringInput
326
+ # input_class(:date) #=> Formtastic::Inputs::DateInput
327
+ #
328
+ # @example When a top-level class is found
329
+ # input_class(:string) #=> StringInput
330
+ # input_class(:awesome) #=> AwesomeInput
331
+ # @see NamespacedClassFinder#find
332
+ def namespaced_input_class(as)
333
+ @input_class_finder ||= input_class_finder.new(self)
334
+ @input_class_finder.find(as)
335
+ rescue Formtastic::InputClassFinder::NotFoundError
336
+ raise Formtastic::UnknownInputError, "Unable to find input #{$!.message}"
337
+ end
338
+
339
+ # @api private
340
+ # @deprecated Use {#namespaced_input_class} instead.
341
+ def input_class(as)
342
+ return namespaced_input_class(as) if input_class_finder
343
+
344
+ input_class_deprecation_warning(__method__)
345
+
346
+ @input_classes_cache ||= {}
347
+ @input_classes_cache[as] ||= begin
348
+ config = Rails.application.config
349
+ use_const_defined = config.respond_to?(:eager_load) ? config.eager_load : config.cache_classes
350
+ use_const_defined ? input_class_with_const_defined(as) : input_class_by_trying(as)
351
+ end
352
+ end
353
+
354
+ # @api private
355
+ # @deprecated Use {InputClassFinder#find} instead.
356
+ # prevent exceptions in production environment for better performance
357
+ def input_class_with_const_defined(as)
358
+ input_class_name = custom_input_class_name(as)
359
+
360
+ if ::Object.const_defined?(input_class_name)
361
+ input_class_name.constantize
362
+ elsif Formtastic::Inputs.const_defined?(input_class_name)
363
+ standard_input_class_name(as).constantize
364
+ else
365
+ raise Formtastic::UnknownInputError, "Unable to find input class #{input_class_name}"
366
+ end
367
+ end
368
+
369
+ # @api private
370
+ # @deprecated Use {InputClassFinder#find} instead.
371
+ # use auto-loading in development environment
372
+ def input_class_by_trying(as)
373
+ begin
374
+ custom_input_class_name(as).constantize
375
+ rescue NameError
376
+ standard_input_class_name(as).constantize
377
+ end
378
+ rescue NameError
379
+ raise Formtastic::UnknownInputError, "Unable to find input class for #{as}"
380
+ end
381
+
382
+ # @api private
383
+ # @deprecated Use {InputClassFinder#class_name} instead.
384
+ # :as => :string # => StringInput
385
+ def custom_input_class_name(as)
386
+ input_class_deprecation_warning(__method__)
387
+ "#{as.to_s.camelize}Input"
388
+ end
389
+
390
+ # @api private
391
+ # @deprecated Use {InputClassFinder#class_name} instead.
392
+ # :as => :string # => {Formtastic::Inputs::StringInput}
393
+ def standard_input_class_name(as)
394
+ input_class_deprecation_warning(__method__)
395
+ "Formtastic::Inputs::#{as.to_s.camelize}Input"
396
+ end
397
+
398
+ private
399
+
400
+ def input_class_deprecation_warning(method)
401
+ @input_class_deprecation_warned ||=
402
+ Formtastic.deprecation.deprecation_warning(method, INPUT_CLASS_DEPRECATION, caller(2))
403
+ end
404
+
405
+ end
406
+ end
407
+ end