formtastic 3.0.0 → 3.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.travis.yml +1 -0
  2. data/Appraisals +4 -0
  3. data/CHANGELOG +12 -22
  4. data/DEPRECATIONS +47 -0
  5. data/README.textile +9 -4
  6. data/formtastic.gemspec +3 -3
  7. data/gemfiles/rails_4.2.gemfile +7 -0
  8. data/lib/formtastic.rb +13 -7
  9. data/lib/formtastic/action_class_finder.rb +18 -0
  10. data/lib/formtastic/deprecation.rb +42 -0
  11. data/lib/formtastic/form_builder.rb +12 -6
  12. data/lib/formtastic/helpers/action_helper.rb +43 -6
  13. data/lib/formtastic/helpers/form_helper.rb +2 -2
  14. data/lib/formtastic/helpers/input_helper.rb +71 -31
  15. data/lib/formtastic/html_attributes.rb +12 -1
  16. data/lib/formtastic/input_class_finder.rb +18 -0
  17. data/lib/formtastic/inputs.rb +1 -0
  18. data/lib/formtastic/inputs/base.rb +11 -12
  19. data/lib/formtastic/inputs/base/choices.rb +1 -1
  20. data/lib/formtastic/inputs/base/collections.rb +1 -1
  21. data/lib/formtastic/inputs/base/html.rb +3 -3
  22. data/lib/formtastic/inputs/datalist_input.rb +41 -0
  23. data/lib/formtastic/inputs/file_input.rb +2 -2
  24. data/lib/formtastic/namespaced_class_finder.rb +89 -0
  25. data/lib/formtastic/util.rb +1 -1
  26. data/lib/formtastic/version.rb +1 -1
  27. data/lib/generators/templates/formtastic.rb +20 -0
  28. data/spec/action_class_finder_spec.rb +12 -0
  29. data/spec/builder/custom_builder_spec.rb +2 -2
  30. data/spec/builder/semantic_fields_for_spec.rb +4 -4
  31. data/spec/helpers/action_helper_spec.rb +9 -355
  32. data/spec/helpers/form_helper_spec.rb +11 -1
  33. data/spec/helpers/input_helper_spec.rb +1 -916
  34. data/spec/helpers/namespaced_action_helper_spec.rb +43 -0
  35. data/spec/helpers/namespaced_input_helper_spec.rb +36 -0
  36. data/spec/input_class_finder_spec.rb +10 -0
  37. data/spec/inputs/check_boxes_input_spec.rb +2 -2
  38. data/spec/inputs/datalist_input_spec.rb +61 -0
  39. data/spec/inputs/select_input_spec.rb +1 -1
  40. data/spec/localizer_spec.rb +2 -2
  41. data/spec/namespaced_class_finder_spec.rb +79 -0
  42. data/spec/spec_helper.rb +17 -7
  43. data/spec/support/custom_macros.rb +22 -4
  44. data/spec/support/shared_examples.rb +1244 -0
  45. data/spec/support/specialized_class_finder_shared_example.rb +27 -0
  46. data/spec/support/test_environment.rb +1 -1
  47. data/spec/util_spec.rb +20 -6
  48. metadata +66 -15
  49. checksums.yaml +0 -15
@@ -36,6 +36,9 @@ module Formtastic
36
36
  # @see Formtastic::Helpers::InputsHelper#inputs
37
37
  # @see Formtastic::Helpers::FormHelper#semantic_form_for
38
38
  module InputHelper
39
+ INPUT_CLASS_DEPRECATION = 'configure Formtastic::FormBuilder.input_class_finder instead'.freeze
40
+ private_constant(:INPUT_CLASS_DEPRECATION)
41
+
39
42
  include Formtastic::Helpers::Reflection
40
43
  include Formtastic::Helpers::FileColumnDetection
41
44
 
@@ -86,26 +89,26 @@ module Formtastic
86
89
  #
87
90
  # Available input styles:
88
91
  #
89
- # * `:boolean` (see {Inputs::BooleanInput})
90
- # * `:check_boxes` (see {Inputs::CheckBoxesInput})
91
- # * `:color` (see {Inputs::ColorInput})
92
- # * `:country` (see {Inputs::CountryInput})
93
- # * `:datetime_select` (see {Inputs::DatetimeSelectInput})
94
- # * `:date_select` (see {Inputs::DateSelectInput})
95
- # * `:email` (see {Inputs::EmailInput})
96
- # * `:file` (see {Inputs::FileInput})
97
- # * `:hidden` (see {Inputs::HiddenInput})
98
- # * `:number` (see {Inputs::NumberInput})
99
- # * `:password` (see {Inputs::PasswordInput})
100
- # * `:phone` (see {Inputs::PhoneInput})
101
- # * `:radio` (see {Inputs::RadioInput})
102
- # * `:search` (see {Inputs::SearchInput})
103
- # * `:select` (see {Inputs::SelectInput})
104
- # * `:string` (see {Inputs::StringInput})
105
- # * `:text` (see {Inputs::TextInput})
106
- # * `:time_zone` (see {Inputs::TimeZoneInput})
107
- # * `:time_select` (see {Inputs::TimeSelectInput})
108
- # * `:url` (see {Inputs::UrlInput})
92
+ # * `:boolean` (see {Inputs::BooleanInput})
93
+ # * `:check_boxes` (see {Inputs::CheckBoxesInput})
94
+ # * `:color` (see {Inputs::ColorInput})
95
+ # * `:country` (see {Inputs::CountryInput})
96
+ # * `:datetime_select` (see {Inputs::DatetimeSelectInput})
97
+ # * `:date_select` (see {Inputs::DateSelectInput})
98
+ # * `:email` (see {Inputs::EmailInput})
99
+ # * `:file` (see {Inputs::FileInput})
100
+ # * `:hidden` (see {Inputs::HiddenInput})
101
+ # * `:number` (see {Inputs::NumberInput})
102
+ # * `:password` (see {Inputs::PasswordInput})
103
+ # * `:phone` (see {Inputs::PhoneInput})
104
+ # * `:radio` (see {Inputs::RadioInput})
105
+ # * `:search` (see {Inputs::SearchInput})
106
+ # * `:select` (see {Inputs::SelectInput})
107
+ # * `:string` (see {Inputs::StringInput})
108
+ # * `:text` (see {Inputs::TextInput})
109
+ # * `:time_zone` (see {Inputs::TimeZoneInput})
110
+ # * `:time_select` (see {Inputs::TimeSelectInput})
111
+ # * `:url` (see {Inputs::UrlInput})
109
112
  #
110
113
  # Calling `:as => :string` (for example) will call `#to_html` on a new instance of
111
114
  # `Formtastic::Inputs::StringInput`. Before this, Formtastic will try to instantiate a top-level
@@ -233,7 +236,7 @@ module Formtastic
233
236
  # @todo Many many more examples. Some of the detail probably needs to be pushed out to the relevant methods too.
234
237
  # @todo More i18n examples.
235
238
  def input(method, options = {})
236
- method = method.to_sym if method.is_a?(String)
239
+ method = method.to_sym
237
240
  options = options.dup # Allow options to be shared without being tainted by Formtastic
238
241
  options[:as] ||= default_input_type(method, options)
239
242
 
@@ -299,15 +302,21 @@ module Formtastic
299
302
 
300
303
  # Get a column object for a specified attribute method - if possible.
301
304
  def column_for(method) #:nodoc:
302
- @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
305
+ if @object.respond_to?(:column_for_attribute)
306
+ # Remove deprecation wrapper & review after Rails 5.0 ships
307
+ ActiveSupport::Deprecation.silence do
308
+ @object.column_for_attribute(method)
309
+ end
310
+ end
303
311
  end
304
312
 
305
- # Takes the `:as` option and attempts to return the corresponding input class. In the case of
306
- # `:as => :string` it will first attempt to find a top level `StringInput` class (to allow the
307
- # application to subclass and modify to suit), falling back to `Formtastic::Inputs::StringInput`.
313
+ # Takes the `:as` option and attempts to return the corresponding input
314
+ # class. In the case of `:as => :awesome` it will first attempt to find a
315
+ # top level `AwesomeInput` class (to allow the application to subclass
316
+ # and modify to suit), falling back to `Formtastic::Inputs::AwesomeInput`.
308
317
  #
309
- # This also means that the application can define it's own custom inputs in the top level
310
- # namespace (eg `DatepickerInput`).
318
+ # Custom input namespaces to look into can be configured via the
319
+ # .input_namespaces +FormBuilder+ configuration setting.
311
320
  #
312
321
  # @param [Symbol] as A symbol representing the type of input to render
313
322
  # @raise [Formtastic::UnknownInputError] An appropriate input class could not be found
@@ -319,8 +328,22 @@ module Formtastic
319
328
  #
320
329
  # @example When a top-level class is found
321
330
  # input_class(:string) #=> StringInput
322
- # input_class(:awesome) #=> AwesomeInput
331
+ # input_class(:awesome) #=> AwesomeInput
332
+
333
+ def namespaced_input_class(as)
334
+ @input_class_finder ||= input_class_finder.new(self)
335
+ @input_class_finder.find(as)
336
+ rescue Formtastic::InputClassFinder::NotFoundError
337
+ raise Formtastic::UnknownInputError, "Unable to find input #{$!.message}"
338
+ end
339
+
340
+ # @api private
341
+ # @deprecated Use {#namespaced_input_class} instead.
323
342
  def input_class(as)
343
+ return namespaced_input_class(as) if input_class_finder
344
+
345
+ input_class_deprecation_warning(__method__)
346
+
324
347
  @input_classes_cache ||= {}
325
348
  @input_classes_cache[as] ||= begin
326
349
  config = Rails.application.config
@@ -328,7 +351,9 @@ module Formtastic
328
351
  use_const_defined ? input_class_with_const_defined(as) : input_class_by_trying(as)
329
352
  end
330
353
  end
331
-
354
+
355
+ # @api private
356
+ # @deprecated Use {InputClassFinder#find} instead.
332
357
  # prevent exceptions in production environment for better performance
333
358
  def input_class_with_const_defined(as)
334
359
  input_class_name = custom_input_class_name(as)
@@ -336,12 +361,14 @@ module Formtastic
336
361
  if ::Object.const_defined?(input_class_name)
337
362
  input_class_name.constantize
338
363
  elsif Formtastic::Inputs.const_defined?(input_class_name)
339
- standard_input_class_name(as).constantize
364
+ standard_input_class_name(as).constantize
340
365
  else
341
366
  raise Formtastic::UnknownInputError, "Unable to find input class #{input_class_name}"
342
367
  end
343
368
  end
344
-
369
+
370
+ # @api private
371
+ # @deprecated Use {InputClassFinder#find} instead.
345
372
  # use auto-loading in development environment
346
373
  def input_class_by_trying(as)
347
374
  begin
@@ -353,16 +380,29 @@ module Formtastic
353
380
  raise Formtastic::UnknownInputError, "Unable to find input class for #{as}"
354
381
  end
355
382
 
383
+ # @api private
384
+ # @deprecated Use {InputClassFinder#class_name} instead.
356
385
  # :as => :string # => StringInput
357
386
  def custom_input_class_name(as)
387
+ input_class_deprecation_warning(__method__)
358
388
  "#{as.to_s.camelize}Input"
359
389
  end
360
390
 
391
+ # @api private
392
+ # @deprecated Use {InputClassFinder#class_name} instead.
361
393
  # :as => :string # => Formtastic::Inputs::StringInput
362
394
  def standard_input_class_name(as)
395
+ input_class_deprecation_warning(__method__)
363
396
  "Formtastic::Inputs::#{as.to_s.camelize}Input"
364
397
  end
365
398
 
399
+ private
400
+
401
+ def input_class_deprecation_warning(method)
402
+ @input_class_deprecation_warned ||=
403
+ Formtastic.deprecation.deprecation_warning(method, INPUT_CLASS_DEPRECATION, caller(2))
404
+ end
405
+
366
406
  end
367
407
  end
368
408
  end
@@ -1,6 +1,17 @@
1
1
  module Formtastic
2
2
  # @private
3
3
  module HtmlAttributes
4
+ # Returns a namespace passed by option or inherited from parent builders / class configuration
5
+ def dom_id_namespace
6
+ namespace = options[:custom_namespace]
7
+ parent = options[:parent_builder]
8
+
9
+ case
10
+ when namespace then namespace
11
+ when parent && parent != self then parent.dom_id_namespace
12
+ else custom_namespace
13
+ end
14
+ end
4
15
 
5
16
  protected
6
17
 
@@ -18,4 +29,4 @@ module Formtastic
18
29
  end
19
30
 
20
31
  end
21
- end
32
+ end
@@ -0,0 +1,18 @@
1
+ module Formtastic
2
+
3
+ # Uses the Formtastic::NamespacedClassFinder to look up input class names.
4
+ #
5
+ # See Formtastic::Helpers::InputHelper#namespaced_input_class for details.
6
+ #
7
+ class InputClassFinder < NamespacedClassFinder
8
+ def initialize(builder)
9
+ super builder.input_namespaces
10
+ end
11
+
12
+ private
13
+
14
+ def class_name(as)
15
+ "#{super}Input"
16
+ end
17
+ end
18
+ end
@@ -8,6 +8,7 @@ module Formtastic
8
8
  autoload :CheckBoxesInput
9
9
  autoload :ColorInput
10
10
  autoload :CountryInput
11
+ autoload :DatalistInput
11
12
  autoload :DateInput
12
13
  autoload :DatePickerInput
13
14
  autoload :DatetimePickerInput
@@ -1,9 +1,9 @@
1
1
  module Formtastic
2
2
  module Inputs
3
3
  module Base
4
-
4
+
5
5
  attr_accessor :builder, :template, :object, :object_name, :method, :options
6
-
6
+
7
7
  def initialize(builder, template, object, object_name, method, options)
8
8
  @builder = builder
9
9
  @template = template
@@ -12,29 +12,29 @@ module Formtastic
12
12
  @method = method
13
13
  @options = options.dup
14
14
  end
15
-
15
+
16
16
  # Usefull for deprecating options.
17
17
  def warn_and_correct_option!(old_option_name, new_option_name)
18
18
  if options.key?(old_option_name)
19
- ::ActiveSupport::Deprecation.warn("The :#{old_option_name} option is deprecated in favour of :#{new_option_name} and will be removed from Formtastic in the next version")
19
+ ::ActiveSupport::Deprecation.warn("The :#{old_option_name} option is deprecated in favour of :#{new_option_name} and will be removed from Formtastic in the next version", caller(6))
20
20
  options[new_option_name] = options.delete(old_option_name)
21
21
  end
22
22
  end
23
-
23
+
24
24
  # Usefull for deprecating options.
25
25
  def warn_deprecated_option!(old_option_name, instructions)
26
26
  if options.key?(old_option_name)
27
- ::ActiveSupport::Deprecation.warn("The :#{old_option_name} option is deprecated in favour of `#{instructions}` and will be removed in the next version")
27
+ ::ActiveSupport::Deprecation.warn("The :#{old_option_name} option is deprecated in favour of `#{instructions}` and will be removed in the next version", caller(6))
28
28
  end
29
29
  end
30
-
30
+
31
31
  # Usefull for raising an error on previously supported option.
32
32
  def removed_option!(old_option_name)
33
33
  raise ArgumentError, ":#{old_option_name} is no longer available" if options.key?(old_option_name)
34
34
  end
35
-
35
+
36
36
  extend ActiveSupport::Autoload
37
-
37
+
38
38
  autoload :DatetimePickerish
39
39
  autoload :Associations
40
40
  autoload :Collections
@@ -53,7 +53,7 @@ module Formtastic
53
53
  autoload :Timeish
54
54
  autoload :Validations
55
55
  autoload :Wrapping
56
-
56
+
57
57
  include Html
58
58
  include Options
59
59
  include Database
@@ -65,8 +65,7 @@ module Formtastic
65
65
  include Associations
66
66
  include Labelling
67
67
  include Wrapping
68
-
68
+
69
69
  end
70
70
  end
71
71
  end
72
-
@@ -73,7 +73,7 @@ module Formtastic
73
73
 
74
74
  def choice_input_dom_id(choice)
75
75
  [
76
- builder.custom_namespace,
76
+ builder.dom_id_namespace,
77
77
  sanitized_object_name,
78
78
  builder.options[:index],
79
79
  association_primary_key || method,
@@ -48,7 +48,7 @@ module Formtastic
48
48
 
49
49
  def collection
50
50
  # Return if we have a plain string
51
- return raw_collection if raw_collection.instance_of?(String) || raw_collection.instance_of?(ActiveSupport::SafeBuffer)
51
+ return raw_collection if raw_collection.is_a?(String)
52
52
 
53
53
  # Return if we have an Array of strings, fixnums or arrays
54
54
  return raw_collection if (raw_collection.instance_of?(Array) || raw_collection.instance_of?(Range)) &&
@@ -28,9 +28,9 @@ module Formtastic
28
28
 
29
29
  def dom_id
30
30
  [
31
- builder.custom_namespace,
32
- sanitized_object_name,
33
- dom_index,
31
+ builder.dom_id_namespace,
32
+ sanitized_object_name,
33
+ dom_index,
34
34
  association_primary_key || sanitized_method_name
35
35
  ].reject { |x| x.blank? }.join('_')
36
36
  end
@@ -0,0 +1,41 @@
1
+ module Formtastic
2
+ module Inputs
3
+ # Outputs a label and a text field, along with a datalist tag
4
+ # datalist tag provides a list of options which drives a simple autocomplete
5
+ # on the text field. This is a HTML5 feature, more info can be found at
6
+ # {https://developer.mozilla.org/en/docs/Web/HTML/Element/datalist <datalist> at MDN}
7
+ # This input accepts a :collection option which takes data in all the usual formats accepted by
8
+ # {http://apidock.com/rails/ActionView/Helpers/FormOptionsHelper/options_for_select options_for_select}
9
+ #
10
+ # @example Input is used as follows
11
+ # f.input :fav_book, :as => :datalist, :collection => Book.pluck(:name)
12
+ #
13
+ class DatalistInput
14
+ include Base
15
+ include Base::Stringish
16
+ include Base::Collections
17
+
18
+ def to_html
19
+ @name = input_html_options[:id].gsub(/_id$/, "")
20
+ input_wrapping do
21
+ label_html <<
22
+ builder.text_field(method, input_html_options) << # standard input
23
+ data_list_html # append new datalist element
24
+ end
25
+ end
26
+
27
+ def input_html_options
28
+ super.merge(:list => html_id_of_datalist)
29
+ end
30
+
31
+ def html_id_of_datalist
32
+ "#{@name}_datalist"
33
+ end
34
+
35
+ def data_list_html
36
+ html = builder.template.options_for_select(collection)
37
+ builder.template.content_tag(:datalist,html, { :id => html_id_of_datalist }, false)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -20,7 +20,7 @@ module Formtastic
20
20
  # <form...>
21
21
  # <fieldset>
22
22
  # <ol>
23
- # <li class="email">
23
+ # <li class="file">
24
24
  # <label for="user_avatar">Avatar</label>
25
25
  # <input type="file" id="user_avatar" name="user[avatar]">
26
26
  # </li>
@@ -29,7 +29,7 @@ module Formtastic
29
29
  # </form>
30
30
  #
31
31
  # @see Formtastic::Helpers::InputsHelper#input InputsHelper#input for full documentation of all possible options.
32
- class FileInput
32
+ class FileInput
33
33
  include Base
34
34
  def to_html
35
35
  input_wrapping do
@@ -0,0 +1,89 @@
1
+ module Formtastic
2
+ # This class implements class resolution in a namespace chain. It
3
+ # is used both by Formtastic::Helpers::InputHelper and
4
+ # Formtastic::Helpers::ActionHelper to look up action and input classes.
5
+ #
6
+ # ==== Example
7
+ # You can implement own class finder that for example prefixes the class name or uses custom module.
8
+ #
9
+ # class MyInputClassFinder < Formtastic::NamespacedClassFinder
10
+ # def initialize(builder)
11
+ # super [MyNamespace, Object] # first lookup in MyNamespace then the globals
12
+ # end
13
+ #
14
+ # private
15
+ #
16
+ # def class_name(as)
17
+ # "My#{super}Input" # for example MyStringInput
18
+ # end
19
+ # end
20
+ #
21
+ # And then set Formtastic::FormBuilder.input_class_finder with that class.
22
+ #
23
+
24
+ class NamespacedClassFinder
25
+ attr_reader :namespaces #:nodoc:
26
+
27
+ # @private
28
+ class NotFoundError < NameError
29
+ end
30
+
31
+ def initialize(namespaces) #:nodoc:
32
+ @namespaces = namespaces.flatten
33
+ @cache = {}
34
+ end
35
+
36
+ # Looks up the given reference in the configured namespaces.
37
+ #
38
+ # Two finder methods are provided, one for development tries to
39
+ # reference the constant directly, triggering Rails' autoloading
40
+ # const_missing machinery; the second one instead for production
41
+ # checks with .const_defined before referencing the constant.
42
+ #
43
+ def find(as)
44
+ @cache[as] ||= resolve(as)
45
+ end
46
+
47
+ def resolve(as)
48
+ class_name = class_name(as)
49
+
50
+ finder(class_name) or raise NotFoundError, "class #{class_name}"
51
+ end
52
+
53
+ private
54
+
55
+ def class_name(as)
56
+ as.to_s.camelize
57
+ end
58
+
59
+ if defined?(Rails) && ::Rails.application && ::Rails.application.config.cache_classes
60
+ def finder(class_name) # :nodoc:
61
+ find_with_const_defined(class_name)
62
+ end
63
+ else
64
+ def finder(class_name) # :nodoc:
65
+ find_by_trying(class_name)
66
+ end
67
+ end
68
+
69
+ # Looks up the given class name in the configured namespaces in order,
70
+ # returning the first one that has the class name constant defined.
71
+ def find_with_const_defined(class_name)
72
+ @namespaces.find do |namespace|
73
+ if namespace.const_defined?(class_name)
74
+ break namespace.const_get(class_name)
75
+ end
76
+ end
77
+ end
78
+
79
+ # Use auto-loading in development environment
80
+ def find_by_trying(class_name)
81
+ @namespaces.find do |namespace|
82
+ begin
83
+ break namespace.const_get(class_name)
84
+ rescue NameError
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end