formtastic 2.1.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.github/workflows/test.yml +61 -0
  4. data/.gitignore +4 -2
  5. data/CHANGELOG.md +52 -0
  6. data/Gemfile +1 -1
  7. data/Gemfile.lock +105 -0
  8. data/MIT-LICENSE +1 -1
  9. data/{README.textile → README.md} +204 -219
  10. data/RELEASE_PROCESS +3 -1
  11. data/Rakefile +27 -29
  12. data/app/assets/stylesheets/formtastic.css +3 -2
  13. data/bin/appraisal +8 -0
  14. data/formtastic.gemspec +11 -14
  15. data/gemfiles/rails_5.2/Gemfile +5 -0
  16. data/gemfiles/rails_6.0/Gemfile +5 -0
  17. data/gemfiles/rails_6.1/Gemfile +5 -0
  18. data/gemfiles/rails_edge/Gemfile +13 -0
  19. data/lib/formtastic/action_class_finder.rb +18 -0
  20. data/lib/formtastic/actions/button_action.rb +55 -60
  21. data/lib/formtastic/actions/input_action.rb +59 -57
  22. data/lib/formtastic/actions/link_action.rb +68 -67
  23. data/lib/formtastic/actions.rb +6 -3
  24. data/lib/formtastic/deprecation.rb +5 -0
  25. data/lib/formtastic/engine.rb +3 -1
  26. data/lib/formtastic/form_builder.rb +35 -16
  27. data/lib/formtastic/helpers/action_helper.rb +34 -28
  28. data/lib/formtastic/helpers/enum.rb +13 -0
  29. data/lib/formtastic/helpers/errors_helper.rb +2 -2
  30. data/lib/formtastic/helpers/fieldset_wrapper.rb +16 -12
  31. data/lib/formtastic/helpers/form_helper.rb +19 -16
  32. data/lib/formtastic/helpers/input_helper.rb +69 -97
  33. data/lib/formtastic/helpers/inputs_helper.rb +35 -25
  34. data/lib/formtastic/helpers/reflection.rb +4 -4
  35. data/lib/formtastic/helpers.rb +1 -2
  36. data/lib/formtastic/html_attributes.rb +12 -1
  37. data/lib/formtastic/i18n.rb +1 -1
  38. data/lib/formtastic/input_class_finder.rb +18 -0
  39. data/lib/formtastic/inputs/base/choices.rb +2 -2
  40. data/lib/formtastic/inputs/base/collections.rb +46 -14
  41. data/lib/formtastic/inputs/base/database.rb +7 -2
  42. data/lib/formtastic/inputs/base/datetime_pickerish.rb +85 -0
  43. data/lib/formtastic/inputs/base/errors.rb +7 -7
  44. data/lib/formtastic/inputs/base/hints.rb +2 -2
  45. data/lib/formtastic/inputs/base/html.rb +10 -9
  46. data/lib/formtastic/inputs/base/labelling.rb +5 -8
  47. data/lib/formtastic/inputs/base/naming.rb +4 -4
  48. data/lib/formtastic/inputs/base/numeric.rb +1 -1
  49. data/lib/formtastic/inputs/base/options.rb +3 -4
  50. data/lib/formtastic/inputs/base/stringish.rb +10 -2
  51. data/lib/formtastic/inputs/base/timeish.rb +34 -22
  52. data/lib/formtastic/inputs/base/validations.rb +41 -13
  53. data/lib/formtastic/inputs/base/wrapping.rb +29 -26
  54. data/lib/formtastic/inputs/base.rb +22 -15
  55. data/lib/formtastic/inputs/boolean_input.rb +26 -12
  56. data/lib/formtastic/inputs/check_boxes_input.rb +39 -31
  57. data/lib/formtastic/inputs/color_input.rb +41 -0
  58. data/lib/formtastic/inputs/country_input.rb +24 -5
  59. data/lib/formtastic/inputs/datalist_input.rb +41 -0
  60. data/lib/formtastic/inputs/date_picker_input.rb +93 -0
  61. data/lib/formtastic/inputs/{date_input.rb → date_select_input.rb} +1 -1
  62. data/lib/formtastic/inputs/datetime_picker_input.rb +103 -0
  63. data/lib/formtastic/inputs/{datetime_input.rb → datetime_select_input.rb} +1 -1
  64. data/lib/formtastic/inputs/file_input.rb +2 -2
  65. data/lib/formtastic/inputs/hidden_input.rb +2 -6
  66. data/lib/formtastic/inputs/radio_input.rb +28 -22
  67. data/lib/formtastic/inputs/select_input.rb +36 -39
  68. data/lib/formtastic/inputs/time_picker_input.rb +99 -0
  69. data/lib/formtastic/inputs/{time_input.rb → time_select_input.rb} +6 -2
  70. data/lib/formtastic/inputs/time_zone_input.rb +16 -6
  71. data/lib/formtastic/inputs.rb +32 -21
  72. data/lib/formtastic/localized_string.rb +1 -1
  73. data/lib/formtastic/localizer.rb +24 -24
  74. data/lib/formtastic/namespaced_class_finder.rb +99 -0
  75. data/lib/formtastic/version.rb +1 -1
  76. data/lib/formtastic.rb +20 -10
  77. data/lib/generators/formtastic/form/form_generator.rb +10 -4
  78. data/lib/generators/formtastic/input/input_generator.rb +46 -0
  79. data/lib/generators/formtastic/install/install_generator.rb +5 -19
  80. data/lib/generators/templates/_form.html.slim +2 -2
  81. data/lib/generators/templates/formtastic.rb +46 -25
  82. data/lib/generators/templates/input.rb +19 -0
  83. data/sample/basic_inputs.html +23 -3
  84. data/script/integration-template.rb +74 -0
  85. data/script/integration.sh +19 -0
  86. data/spec/action_class_finder_spec.rb +12 -0
  87. data/spec/actions/button_action_spec.rb +8 -8
  88. data/spec/actions/generic_action_spec.rb +92 -56
  89. data/spec/actions/input_action_spec.rb +7 -7
  90. data/spec/actions/link_action_spec.rb +10 -10
  91. data/spec/builder/custom_builder_spec.rb +36 -20
  92. data/spec/builder/error_proc_spec.rb +4 -4
  93. data/spec/builder/semantic_fields_for_spec.rb +28 -29
  94. data/spec/fast_spec_helper.rb +12 -0
  95. data/spec/generators/formtastic/form/form_generator_spec.rb +45 -32
  96. data/spec/generators/formtastic/input/input_generator_spec.rb +124 -0
  97. data/spec/generators/formtastic/install/install_generator_spec.rb +9 -9
  98. data/spec/helpers/action_helper_spec.rb +75 -103
  99. data/spec/helpers/actions_helper_spec.rb +17 -17
  100. data/spec/helpers/form_helper_spec.rb +84 -33
  101. data/spec/helpers/input_helper_spec.rb +333 -285
  102. data/spec/helpers/inputs_helper_spec.rb +167 -121
  103. data/spec/helpers/reflection_helper_spec.rb +3 -3
  104. data/spec/helpers/semantic_errors_helper_spec.rb +23 -23
  105. data/spec/i18n_spec.rb +26 -26
  106. data/spec/input_class_finder_spec.rb +10 -0
  107. data/spec/inputs/base/collections_spec.rb +76 -0
  108. data/spec/inputs/base/validations_spec.rb +480 -0
  109. data/spec/inputs/boolean_input_spec.rb +100 -65
  110. data/spec/inputs/check_boxes_input_spec.rb +200 -101
  111. data/spec/inputs/color_input_spec.rb +85 -0
  112. data/spec/inputs/country_input_spec.rb +20 -20
  113. data/spec/inputs/custom_input_spec.rb +3 -4
  114. data/spec/inputs/datalist_input_spec.rb +61 -0
  115. data/spec/inputs/date_picker_input_spec.rb +449 -0
  116. data/spec/inputs/date_select_input_spec.rb +249 -0
  117. data/spec/inputs/datetime_picker_input_spec.rb +490 -0
  118. data/spec/inputs/datetime_select_input_spec.rb +209 -0
  119. data/spec/inputs/email_input_spec.rb +5 -5
  120. data/spec/inputs/file_input_spec.rb +6 -6
  121. data/spec/inputs/hidden_input_spec.rb +22 -35
  122. data/spec/inputs/include_blank_spec.rb +11 -11
  123. data/spec/inputs/label_spec.rb +62 -25
  124. data/spec/inputs/number_input_spec.rb +112 -112
  125. data/spec/inputs/password_input_spec.rb +5 -5
  126. data/spec/inputs/phone_input_spec.rb +5 -5
  127. data/spec/inputs/placeholder_spec.rb +6 -6
  128. data/spec/inputs/radio_input_spec.rb +99 -55
  129. data/spec/inputs/range_input_spec.rb +66 -66
  130. data/spec/inputs/readonly_spec.rb +50 -0
  131. data/spec/inputs/search_input_spec.rb +5 -5
  132. data/spec/inputs/select_input_spec.rb +170 -170
  133. data/spec/inputs/string_input_spec.rb +68 -16
  134. data/spec/inputs/text_input_spec.rb +16 -16
  135. data/spec/inputs/time_picker_input_spec.rb +455 -0
  136. data/spec/inputs/time_select_input_spec.rb +261 -0
  137. data/spec/inputs/time_zone_input_spec.rb +54 -28
  138. data/spec/inputs/url_input_spec.rb +5 -5
  139. data/spec/inputs/with_options_spec.rb +7 -7
  140. data/spec/localizer_spec.rb +39 -17
  141. data/spec/namespaced_class_finder_spec.rb +79 -0
  142. data/spec/schema.rb +21 -0
  143. data/spec/spec_helper.rb +254 -221
  144. data/spec/support/custom_macros.rb +128 -95
  145. data/spec/support/shared_examples.rb +12 -0
  146. data/spec/support/specialized_class_finder_shared_example.rb +27 -0
  147. data/spec/support/test_environment.rb +26 -10
  148. metadata +177 -238
  149. data/.travis.yml +0 -8
  150. data/Appraisals +0 -11
  151. data/CHANGELOG +0 -371
  152. data/gemfiles/rails-3.0.gemfile +0 -7
  153. data/gemfiles/rails-3.1.gemfile +0 -7
  154. data/gemfiles/rails-3.2.gemfile +0 -7
  155. data/lib/formtastic/helpers/buttons_helper.rb +0 -310
  156. data/lib/formtastic/inputs/base/grouped_collections.rb +0 -77
  157. data/lib/formtastic/util.rb +0 -25
  158. data/lib/tasks/verify_rcov.rb +0 -44
  159. data/spec/helpers/buttons_helper_spec.rb +0 -166
  160. data/spec/helpers/commit_button_helper_spec.rb +0 -530
  161. data/spec/inputs/date_input_spec.rb +0 -227
  162. data/spec/inputs/datetime_input_spec.rb +0 -185
  163. data/spec/inputs/time_input_spec.rb +0 -267
  164. data/spec/support/deferred_garbage_collection.rb +0 -21
@@ -1,9 +1,18 @@
1
1
  module Formtastic
2
2
  class FormBuilder < ActionView::Helpers::FormBuilder
3
3
 
4
- def self.configure(name, value = nil)
4
+ # Defines a new configurable option
5
+ # @param [Symbol] name the configuration name
6
+ # @param [Object] default the configuration default value
7
+ # @private
8
+ #
9
+ # @!macro [new] configure
10
+ # @!scope class
11
+ # @!attribute [rw] $1
12
+ # @api public
13
+ def self.configure(name, default = nil)
5
14
  class_attribute(name)
6
- self.send(:"#{name}=", value)
15
+ self.send(:"#{name}=", default)
7
16
  end
8
17
 
9
18
  configure :custom_namespace
@@ -12,13 +21,12 @@ module Formtastic
12
21
  configure :default_text_area_width
13
22
  configure :all_fields_required_by_default, true
14
23
  configure :include_blank_for_select_by_default, true
15
- configure :required_string, proc { Formtastic::Util.html_safe(%{<abbr title="#{Formtastic::I18n.t(:required)}">*</abbr>}) }
24
+ configure :required_string, proc { %{<abbr title="#{Formtastic::I18n.t(:required)}">*</abbr>}.html_safe }
16
25
  configure :optional_string, ''
17
26
  configure :inline_errors, :sentence
18
27
  configure :label_str_method, :humanize
19
28
  configure :collection_label_methods, %w[to_label display_name full_name name title username login value to_s]
20
29
  configure :collection_value_methods, %w[id to_s]
21
- configure :custom_inline_order, {}
22
30
  configure :file_methods, [ :file?, :public_filename, :filename ]
23
31
  configure :file_metadata_suffixes, ['content_type', 'file_name', 'file_size']
24
32
  configure :priority_countries, ["Australia", "Canada", "United Kingdom", "United States"]
@@ -32,6 +40,15 @@ module Formtastic
32
40
  configure :default_hint_class, 'inline-hints'
33
41
  configure :use_required_attribute, false
34
42
  configure :perform_browser_validations, false
43
+ # Check {Formtastic::InputClassFinder} to see how are inputs resolved.
44
+ configure :input_namespaces, [::Object, ::Formtastic::Inputs]
45
+ configure :input_class_finder, Formtastic::InputClassFinder
46
+ # Check {Formtastic::ActionClassFinder} to see how are inputs resolved.
47
+ configure :action_namespaces, [::Object, ::Formtastic::Actions]
48
+ configure :action_class_finder, Formtastic::ActionClassFinder
49
+
50
+ configure :skipped_columns, [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
51
+ configure :priority_time_zones, []
35
52
 
36
53
  attr_reader :template
37
54
 
@@ -41,15 +58,14 @@ module Formtastic
41
58
 
42
59
  include Formtastic::Helpers::InputHelper
43
60
  include Formtastic::Helpers::InputsHelper
44
- include Formtastic::Helpers::ButtonsHelper
45
61
  include Formtastic::Helpers::ActionHelper
46
62
  include Formtastic::Helpers::ActionsHelper
47
63
  include Formtastic::Helpers::ErrorsHelper
48
-
49
- # This is a wrapper around Rails' `ActionView::Helpers::FormBuilder#fields_for`, originally
64
+
65
+ # This is a wrapper around Rails' `ActionView::Helpers::FormBuilder#fields_for`, originally
50
66
  # provided to ensure that the `:builder` from `semantic_form_for` was passed down into
51
- # the nested `fields_for`. Rails 3 no longer requires us to do this, so this method is
52
- # provided purely for backwards compatibility and DSL consistency.
67
+ # the nested `fields_for`. Our supported versions of Rails no longer require us to do this,
68
+ # so this method is provided purely for backwards compatibility and DSL consistency.
53
69
  #
54
70
  # When constructing a `fields_for` form fragment *outside* of `semantic_form_for`, please use
55
71
  # `Formtastic::Helpers::FormHelper#semantic_fields_for`.
@@ -75,14 +91,17 @@ module Formtastic
75
91
  #
76
92
  # @todo is there a way to test the params structure of the Rails helper we wrap to ensure forward compatibility?
77
93
  def semantic_fields_for(record_or_name_or_array, *args, &block)
78
- # Add a :parent_builder to the args so that nested translations can be possible in Rails 3
79
- options = args.extract_options!
80
- options[:parent_builder] ||= self
81
-
82
- # Wrap the Rails helper
83
- fields_for(record_or_name_or_array, *(args << options), &block)
94
+ fields_for(record_or_name_or_array, *args, &block)
84
95
  end
85
-
96
+
97
+ def initialize(object_name, object, template, options)
98
+ super
99
+
100
+ if respond_to?('multipart=') && options.is_a?(Hash) && options[:html]
101
+ self.multipart = options[:html][:multipart]
102
+ end
103
+ end
104
+
86
105
  end
87
106
 
88
107
  end
@@ -2,7 +2,6 @@
2
2
  module Formtastic
3
3
  module Helpers
4
4
  module ActionHelper
5
-
6
5
  # Renders an action for the form (such as a subit/reset button, or a cancel link).
7
6
  #
8
7
  # Each action is wrapped in an `<li class="action">` tag with other classes added based on the
@@ -12,6 +11,22 @@ module Formtastic
12
11
  # The textual value of the label can be changed from the default through the `:label`
13
12
  # argument or through i18n.
14
13
  #
14
+ # If using i18n, you'll need to provide the following translations:
15
+ #
16
+ # en:
17
+ # formtastic:
18
+ # actions:
19
+ # create: "Create new %{model}"
20
+ # update: "Save %{model}"
21
+ # cancel: "Cancel"
22
+ # reset: "Reset form"
23
+ # submit: "Submit"
24
+ #
25
+ # For forms with an object present, the `update` key will be used if calling `persisted?` on
26
+ # the object returns true (saving changes to a record), otherwise the `create` key will be
27
+ # used. The `submit` key is used as a fallback when there is no object or we cannot determine
28
+ # if `create` or `update` is appropriate.
29
+ #
15
30
  # @example Basic usage
16
31
  # # form
17
32
  # <%= semantic_form_for @post do |f| %>
@@ -64,46 +79,37 @@ module Formtastic
64
79
  options = options.dup # Allow options to be shared without being tainted by Formtastic
65
80
  options[:as] ||= default_action_type(method, options)
66
81
 
67
- klass = action_class(options[:as])
82
+ klass = namespaced_action_class(options[:as])
68
83
 
69
84
  klass.new(self, template, @object, @object_name, method, options).to_html
70
85
  end
71
86
 
72
87
  protected
73
88
 
74
- def default_action_type(method, options = {}) #:nodoc:
89
+ def default_action_type(method, options = {}) # @private
75
90
  case method
76
91
  when :submit then :input
77
- when :reset then :input
92
+ when :reset then :input
78
93
  when :cancel then :link
94
+ else method
79
95
  end
80
96
  end
81
97
 
82
- def action_class(as)
83
- @input_classes_cache ||= {}
84
- @input_classes_cache[as] ||= begin
85
- begin
86
- begin
87
- custom_action_class_name(as).constantize
88
- rescue NameError
89
- standard_action_class_name(as).constantize
90
- end
91
- rescue NameError
92
- raise Formtastic::UnknownActionError
93
- end
94
- end
95
- end
96
-
97
- # :as => :button # => ButtonAction
98
- def custom_action_class_name(as)
99
- "#{as.to_s.camelize}Action"
100
- end
101
-
102
- # :as => :button # => Formtastic::Actions::ButtonAction
103
- def standard_action_class_name(as)
104
- "Formtastic::Actions::#{as.to_s.camelize}Action"
98
+ # Takes the `:as` option and attempts to return the corresponding action
99
+ # class. In the case of `:as => :awesome` it will first attempt to find a
100
+ # top level `AwesomeAction` class (to allow the application to subclass
101
+ # and modify to suit), falling back to `Formtastic::Actions::AwesomeAction`.
102
+ #
103
+ # Custom action namespaces to look into can be configured via the
104
+ # {Formtastic::FormBuilder.action_namespaces} configuration setting.
105
+ # @see Helpers::InputHelper#namespaced_input_class
106
+ # @see Formtastic::ActionClassFinder
107
+ def namespaced_action_class(as)
108
+ @action_class_finder ||= action_class_finder.new(self)
109
+ @action_class_finder.find(as)
110
+ rescue Formtastic::ActionClassFinder::NotFoundError => e
111
+ raise Formtastic::UnknownActionError, "Unable to find action #{e.message}"
105
112
  end
106
-
107
113
  end
108
114
  end
109
115
  end
@@ -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
@@ -52,7 +52,7 @@ module Formtastic
52
52
  return nil if full_errors.blank?
53
53
  html_options[:class] ||= "errors"
54
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)
55
+ full_errors.map { |error| template.content_tag(:li, error) }.join.html_safe
56
56
  end
57
57
  end
58
58
 
@@ -78,4 +78,4 @@ module Formtastic
78
78
  end
79
79
  end
80
80
  end
81
- end
81
+ end
@@ -21,8 +21,8 @@ module Formtastic
21
21
  # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
22
22
  # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
23
23
  # f.inputs :title, :body, :author # First argument is a column => (no legend)
24
- def field_set_and_list_wrapping(*args, &block) #:nodoc:
25
- contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
24
+ def field_set_and_list_wrapping(*args, &block) # @private
25
+ contents = args[-1].is_a?(::Hash) ? '' : args.pop.flatten
26
26
  html_options = args.extract_options!
27
27
 
28
28
  if block_given?
@@ -33,12 +33,15 @@ module Formtastic
33
33
  end
34
34
  end
35
35
 
36
+ # Work-around for empty contents block
37
+ contents ||= ""
38
+
36
39
  # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
37
40
  contents = contents.join if contents.respond_to?(:join)
38
41
 
39
42
  legend = field_set_legend(html_options)
40
43
  fieldset = template.content_tag(:fieldset,
41
- Formtastic::Util.html_safe(legend) << template.content_tag(:ol, Formtastic::Util.html_safe(contents)),
44
+ legend.html_safe << template.content_tag(:ol, contents.html_safe),
42
45
  html_options.except(:builder, :parent, :name)
43
46
  )
44
47
 
@@ -47,32 +50,33 @@ module Formtastic
47
50
 
48
51
  def field_set_legend(html_options)
49
52
  legend = (html_options[:name] || '').to_s
50
- legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
51
- legend = template.content_tag(:legend, template.content_tag(:span, Formtastic::Util.html_safe(legend))) unless legend.blank?
53
+ # only applying if String includes '%i' avoids argument error when $DEBUG is true
54
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent] && legend.include?('%i')
55
+ legend = template.content_tag(:legend, template.content_tag(:span, legend.html_safe)) unless legend.blank?
52
56
  legend
53
57
  end
54
58
 
55
59
  # Gets the nested_child_index value from the parent builder. It returns a hash with each
56
60
  # association that the parent builds.
57
- def parent_child_index(parent) #:nodoc:
61
+ def parent_child_index(parent) # @private
58
62
  # Could be {"post[authors_attributes]"=>0} or { :authors => 0 }
59
63
  duck = parent[:builder].instance_variable_get('@nested_child_index')
60
-
64
+
61
65
  # Could be symbol for the association, or a model (or an array of either, I think? TODO)
62
66
  child = parent[:for]
63
67
  # Pull a sybol or model out of Array (TODO: check if there's an Array)
64
68
  child = child.first if child.respond_to?(:first)
65
69
  # If it's an object, get a symbol from the class name
66
70
  child = child.class.name.underscore.to_sym unless child.is_a?(Symbol)
67
-
71
+
68
72
  key = "#{parent[:builder].object_name}[#{child}_attributes]"
69
73
 
70
- # TODO: One of the tests produces a scenario where duck is "0" and the test looks for a "1"
74
+ # TODO: One of the tests produces a scenario where duck is "0" and the test looks for a "1"
71
75
  # 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?(Fixnum)
73
-
76
+ return duck + 1 if duck.is_a?(Integer)
77
+
74
78
  # First try to extract key from duck Hash, then try child
75
- i = (duck[key] || duck[child]).to_i + 1
79
+ (duck[key] || duck[child]).to_i + 1
76
80
  end
77
81
 
78
82
  end
@@ -58,6 +58,17 @@ module Formtastic
58
58
  @@default_form_class = 'formtastic'
59
59
  mattr_accessor :default_form_class
60
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
+
61
72
  # Wrapper around Rails' own `form_for` helper to set the `:builder` option to
62
73
  # `Formtastic::FormBuilder` and to set some class names on the `<form>` tag such as
63
74
  # `formtastic` and the downcased and underscored model name (eg `post`).
@@ -144,18 +155,20 @@ module Formtastic
144
155
  options[:builder] ||= @@builder
145
156
  options[:html] ||= {}
146
157
  options[:html][:novalidate] = !@@builder.perform_browser_validations unless options[:html].key?(:novalidate)
147
- @@builder.custom_namespace = options.delete(:namespace).to_s
158
+ options[:custom_namespace] = options.delete(:namespace)
148
159
 
149
160
  singularizer = defined?(ActiveModel::Naming.singular) ? ActiveModel::Naming.method(:singular) : ActionController::RecordIdentifier.method(:singular_class_name)
150
161
 
151
162
  class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
152
163
  class_names << @@default_form_class
153
- class_names << case record_or_name_or_array
164
+ model_class_name = case record_or_name_or_array
154
165
  when String, Symbol then record_or_name_or_array.to_s # :post => "post"
155
- when Array then options[:as] || singularizer.call(record_or_name_or_array.last.class) # [@post, @comment] # => "comment"
166
+ when Array then options[:as] || singularizer.call(record_or_name_or_array[-1].class) # [@post, @comment] # => "comment"
156
167
  else options[:as] || singularizer.call(record_or_name_or_array.class) # @post => "post"
157
168
  end
158
- options[:html][:class] = class_names.join(" ")
169
+ class_names << @@default_form_model_class_proc.call(model_class_name)
170
+
171
+ options[:html][:class] = class_names.compact.join(" ")
159
172
 
160
173
  with_custom_field_error_proc do
161
174
  self.form_for(record_or_name_or_array, *(args << options), &proc)
@@ -169,7 +182,7 @@ module Formtastic
169
182
  def semantic_fields_for(record_name, record_object = nil, options = {}, &block)
170
183
  options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
171
184
  options[:builder] ||= @@builder
172
- @@builder.custom_namespace = options.delete(:namespace).to_s # TODO needed?
185
+ options[:custom_namespace] = options.delete(:namespace)
173
186
 
174
187
  with_custom_field_error_proc do
175
188
  self.fields_for(record_name, record_object, options, &block)
@@ -178,23 +191,13 @@ module Formtastic
178
191
 
179
192
  protected
180
193
 
181
- # Override the default ActiveRecordHelper behaviour of wrapping the input.
182
- # This gets taken care of semantically by adding an error class to the LI tag
183
- # containing the input.
184
- # @private
185
- FIELD_ERROR_PROC = proc do |html_tag, instance_tag|
186
- html_tag
187
- end
188
-
189
194
  def with_custom_field_error_proc(&block)
190
195
  default_field_error_proc = ::ActionView::Base.field_error_proc
191
- ::ActionView::Base.field_error_proc = FIELD_ERROR_PROC
196
+ ::ActionView::Base.field_error_proc = @@formtastic_field_error_proc
192
197
  yield
193
198
  ensure
194
199
  ::ActionView::Base.field_error_proc = default_field_error_proc
195
200
  end
196
-
197
-
198
201
  end
199
202
  end
200
203
  end
@@ -37,6 +37,7 @@ module Formtastic
37
37
  # @see Formtastic::Helpers::FormHelper#semantic_form_for
38
38
  module InputHelper
39
39
  include Formtastic::Helpers::Reflection
40
+ include Formtastic::Helpers::Enum
40
41
  include Formtastic::Helpers::FileColumnDetection
41
42
 
42
43
  # Returns a chunk of HTML markup for a given `method` on the form object, wrapped in
@@ -86,25 +87,26 @@ module Formtastic
86
87
  #
87
88
  # Available input styles:
88
89
  #
89
- # * `:boolean` (see {Inputs::BooleanInput})
90
- # * `:check_boxes` (see {Inputs::CheckBoxesInput})
91
- # * `:country` (see {Inputs::CountryInput})
92
- # * `:datetime` (see {Inputs::DatetimeInput})
93
- # * `:date` (see {Inputs::DateInput})
94
- # * `:email` (see {Inputs::EmailInput})
95
- # * `:file` (see {Inputs::FileInput})
96
- # * `:hidden` (see {Inputs::HiddenInput})
97
- # * `:number` (see {Inputs::NumberInput})
98
- # * `:password` (see {Inputs::PasswordInput})
99
- # * `:phone` (see {Inputs::PhoneInput})
100
- # * `:radio` (see {Inputs::RadioInput})
101
- # * `:search` (see {Inputs::SearchInput})
102
- # * `:select` (see {Inputs::SelectInput})
103
- # * `:string` (see {Inputs::StringInput})
104
- # * `:text` (see {Inputs::TextInput})
105
- # * `:time_zone` (see {Inputs::TimeZoneInput})
106
- # * `:time` (see {Inputs::TimeInput})
107
- # * `:url` (see {Inputs::UrlInput})
90
+ # * `:boolean` (see {Inputs::BooleanInput})
91
+ # * `:check_boxes` (see {Inputs::CheckBoxesInput})
92
+ # * `:color` (see {Inputs::ColorInput})
93
+ # * `:country` (see {Inputs::CountryInput})
94
+ # * `:datetime_select` (see {Inputs::DatetimeSelectInput})
95
+ # * `:date_select` (see {Inputs::DateSelectInput})
96
+ # * `:email` (see {Inputs::EmailInput})
97
+ # * `:file` (see {Inputs::FileInput})
98
+ # * `:hidden` (see {Inputs::HiddenInput})
99
+ # * `:number` (see {Inputs::NumberInput})
100
+ # * `:password` (see {Inputs::PasswordInput})
101
+ # * `:phone` (see {Inputs::PhoneInput})
102
+ # * `:radio` (see {Inputs::RadioInput})
103
+ # * `:search` (see {Inputs::SearchInput})
104
+ # * `:select` (see {Inputs::SelectInput})
105
+ # * `:string` (see {Inputs::StringInput})
106
+ # * `:text` (see {Inputs::TextInput})
107
+ # * `:time_zone` (see {Inputs::TimeZoneInput})
108
+ # * `:time_select` (see {Inputs::TimeSelectInput})
109
+ # * `:url` (see {Inputs::UrlInput})
108
110
  #
109
111
  # Calling `:as => :string` (for example) will call `#to_html` on a new instance of
110
112
  # `Formtastic::Inputs::StringInput`. Before this, Formtastic will try to instantiate a top-level
@@ -130,6 +132,7 @@ module Formtastic
130
132
  #
131
133
  # @option options :input_html [Hash]
132
134
  # Override or add to the HTML attributes to be passed down to the `<input>` tag
135
+ # (If you use attr_readonly method in your model, formtastic will automatically set those attributes's input readonly)
133
136
  #
134
137
  # @option options :wrapper_html [Hash]
135
138
  # Override or add to the HTML attributes to be passed down to the wrapping `<li>` tag
@@ -137,40 +140,16 @@ module Formtastic
137
140
  # @option options :collection [Array<ActiveModel, String, Symbol>, Hash{String => String, Boolean}, OrderedHash{String => String, Boolean}]
138
141
  # Override collection of objects in the association (`:select`, `:radio` & `:check_boxes` inputs only)
139
142
  #
140
- # @option options :member_label [Symbol, Proc, Method]
141
- # Override the method called on each object in the `:collection` for use as the `<label>` content (`:check_boxes` & `:radio` inputs) or `<option>` content (`:select` inputs)
142
- #
143
- # @option options :member_value [Symbol, Proc, Method]
144
- # Override the method called on each object in the `:collection` for use as the `value` attribute in the `<input>` (`:check_boxes` & `:radio` inputs) or `<option>` (`:select` inputs)
145
- #
146
- # @option options :hint_class [String]
147
- # Override the `class` attribute applied to the `<p>` tag used when a `:hint` is rendered for an input
148
- #
149
- # @option options :error_class [String]
150
- # Override the `class` attribute applied to the `<p>` or `<ol>` tag used when inline errors are rendered for an input
151
- #
152
143
  # @option options :multiple [Boolean]
153
144
  # 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)
154
145
  #
155
- # @option options :group_by [Symbol]
156
- # TODO will probably be deprecated
157
- #
158
- # @option options :find_options [Symbol]
159
- # TODO will probably be deprecated
160
- #
161
- # @option options :group_label [Symbol]
162
- # TODO will probably be deprecated
163
- #
164
146
  # @option options :include_blank [Boolean]
165
147
  # Specify if a `:select` input should include a blank option or not (defaults to `include_blank_for_select_by_default` configuration)
166
148
  #
167
149
  # @option options :prompt [String]
168
150
  # Specify the text in the first ('blank') `:select` input `<option>` to prompt a user to make a selection (implicitly sets `:include_blank` to `true`)
169
151
  #
170
- # @todo Can we kill `:hint_class` & `:error_class`? What's the use case for input-by-input? Shift to config or burn!
171
- # @todo Can we kill `:group_by` & `:group_label`? Should be done with :collection => grouped_options_for_select(...)
172
- # @todo Can we kill `:find_options`? Should be done with MyModel.some_scope.where(...).order(...).whatever_scope
173
- # @todo Can we kill `:label`, `:hint` & `:prompt`? All strings could be shifted to i18n!
152
+ # @todo Can we deprecate & kill `:label`, `:hint` & `:prompt`? All strings could be shifted to i18n!
174
153
  #
175
154
  # @example Accept all default options
176
155
  # <%= f.input :title %>
@@ -247,13 +226,15 @@ module Formtastic
247
226
  # first_name: "Joe"
248
227
  # last_name: "Smith"
249
228
  #
229
+ # @see #namespaced_input_class
250
230
  # @todo Many many more examples. Some of the detail probably needs to be pushed out to the relevant methods too.
251
231
  # @todo More i18n examples.
252
232
  def input(method, options = {})
233
+ method = method.to_sym
253
234
  options = options.dup # Allow options to be shared without being tainted by Formtastic
254
235
  options[:as] ||= default_input_type(method, options)
255
236
 
256
- klass = input_class(options[:as])
237
+ klass = namespaced_input_class(options[:as])
257
238
 
258
239
  klass.new(self, template, @object, @object_name, method, options).to_html
259
240
  end
@@ -270,14 +251,15 @@ module Formtastic
270
251
  #
271
252
  # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
272
253
  # default is a :string, a similar behaviour to Rails' scaffolding.
273
- def default_input_type(method, options = {}) #:nodoc:
254
+ def default_input_type(method, options = {}) # @private
274
255
  if @object
275
256
  return :select if reflection_for(method)
276
257
 
277
258
  return :file if is_file?(method, options)
278
259
  end
279
260
 
280
- if column = column_for(method)
261
+ column = column_for(method)
262
+ if column && column.type
281
263
  # Special cases where the column type doesn't map to an input method.
282
264
  case column.type
283
265
  when :string
@@ -288,13 +270,23 @@ module Formtastic
288
270
  return :url if method.to_s =~ /^url$|^website$|_url$/
289
271
  return :phone if method.to_s =~ /(phone|fax)/
290
272
  return :search if method.to_s =~ /^search$/
273
+ return :color if method.to_s =~ /color/
291
274
  when :integer
292
275
  return :select if reflection_for(method)
276
+ return :select if enum_for(method)
293
277
  return :number
294
278
  when :float, :decimal
295
279
  return :number
296
- when :timestamp
297
- return :datetime
280
+ when :datetime, :timestamp
281
+ return :datetime_select
282
+ when :time
283
+ return :time_select
284
+ when :date
285
+ return :date_select
286
+ when :hstore, :json, :jsonb
287
+ return :text
288
+ when :citext, :inet
289
+ return :string
298
290
  end
299
291
 
300
292
  # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
@@ -309,16 +301,30 @@ module Formtastic
309
301
  end
310
302
 
311
303
  # Get a column object for a specified attribute method - if possible.
312
- def column_for(method) #:nodoc:
313
- @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
304
+ # @return [ActiveModel::Type::Value, #type] in case of rails 5 attributes api
305
+ # @return [ActiveRecord::ConnectionAdapters::Column] in case of rails 4
306
+ def column_for(method) # @private
307
+ case
308
+ when @object.class.respond_to?(:type_for_attribute)
309
+ @object.class.type_for_attribute(method.to_s)
310
+ when @object.class.respond_to?(:column_for_attribute)
311
+ @object.class.column_for_attribute(method)
312
+ when @object.respond_to?(:column_for_attribute)
313
+ # Remove deprecation wrapper & review after Rails 5.0 ships
314
+ ActiveSupport::Deprecation.silence do
315
+ @object.column_for_attribute(method)
316
+ end
317
+ else nil
318
+ end
314
319
  end
315
320
 
316
- # Takes the `:as` option and attempts to return the corresponding input class. In the case of
317
- # `:as => :string` it will first attempt to find a top level `StringInput` class (to allow the
318
- # application to subclass and modify to suit), falling back to `Formtastic::Inputs::StringInput`.
321
+ # Takes the `:as` option and attempts to return the corresponding input
322
+ # class. In the case of `:as => :awesome` it will first attempt to find a
323
+ # top level `AwesomeInput` class (to allow the application to subclass
324
+ # and modify to suit), falling back to `Formtastic::Inputs::AwesomeInput`.
319
325
  #
320
- # This also means that the application can define it's own custom inputs in the top level
321
- # namespace (eg `DatepickerInput`).
326
+ # Custom input namespaces to look into can be configured via the
327
+ # {Formtastic::FormBuilder.input_namespaces} configuration setting.
322
328
  #
323
329
  # @param [Symbol] as A symbol representing the type of input to render
324
330
  # @raise [Formtastic::UnknownInputError] An appropriate input class could not be found
@@ -330,48 +336,14 @@ module Formtastic
330
336
  #
331
337
  # @example When a top-level class is found
332
338
  # input_class(:string) #=> StringInput
333
- # input_class(:awesome) #=> AwesomeInput
334
- def input_class(as)
335
- @input_classes_cache ||= {}
336
- @input_classes_cache[as] ||= begin
337
- Rails.application.config.cache_classes ? input_class_with_const_defined(as) : input_class_by_trying(as)
338
- end
339
- end
340
-
341
- # prevent exceptions in production environment for better performance
342
- def input_class_with_const_defined(as)
343
- input_class_name = custom_input_class_name(as)
344
-
345
- if ::Object.const_defined?(input_class_name)
346
- input_class_name.constantize
347
- elsif Formtastic::Inputs.const_defined?(input_class_name)
348
- standard_input_class_name(as).constantize
349
- else
350
- raise Formtastic::UnknownInputError
351
- end
352
- end
353
-
354
- # use auto-loading in development environment
355
- def input_class_by_trying(as)
356
- begin
357
- custom_input_class_name(as).constantize
358
- rescue NameError
359
- standard_input_class_name(as).constantize
360
- end
361
- rescue NameError
362
- raise Formtastic::UnknownInputError
339
+ # input_class(:awesome) #=> AwesomeInput
340
+ # @see NamespacedClassFinder#find
341
+ def namespaced_input_class(as)
342
+ @input_class_finder ||= input_class_finder.new(self)
343
+ @input_class_finder.find(as)
344
+ rescue Formtastic::InputClassFinder::NotFoundError
345
+ raise Formtastic::UnknownInputError, "Unable to find input #{$!.message}"
363
346
  end
364
-
365
- # :as => :string # => StringInput
366
- def custom_input_class_name(as)
367
- "#{as.to_s.camelize}Input"
368
- end
369
-
370
- # :as => :string # => Formtastic::Inputs::StringInput
371
- def standard_input_class_name(as)
372
- "Formtastic::Inputs::#{as.to_s.camelize}Input"
373
- end
374
-
375
347
  end
376
348
  end
377
349
  end