formtastic 3.0.0 → 3.1.0.rc1

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 (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