formtastic 1.2.4 → 3.1.5

Sign up to get free protection for your applications and to get access to all the features.
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