formtastic 2.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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