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.
- data/.travis.yml +1 -0
- data/Appraisals +4 -0
- data/CHANGELOG +12 -22
- data/DEPRECATIONS +47 -0
- data/README.textile +9 -4
- data/formtastic.gemspec +3 -3
- data/gemfiles/rails_4.2.gemfile +7 -0
- data/lib/formtastic.rb +13 -7
- data/lib/formtastic/action_class_finder.rb +18 -0
- data/lib/formtastic/deprecation.rb +42 -0
- data/lib/formtastic/form_builder.rb +12 -6
- data/lib/formtastic/helpers/action_helper.rb +43 -6
- data/lib/formtastic/helpers/form_helper.rb +2 -2
- data/lib/formtastic/helpers/input_helper.rb +71 -31
- data/lib/formtastic/html_attributes.rb +12 -1
- data/lib/formtastic/input_class_finder.rb +18 -0
- data/lib/formtastic/inputs.rb +1 -0
- data/lib/formtastic/inputs/base.rb +11 -12
- data/lib/formtastic/inputs/base/choices.rb +1 -1
- data/lib/formtastic/inputs/base/collections.rb +1 -1
- data/lib/formtastic/inputs/base/html.rb +3 -3
- data/lib/formtastic/inputs/datalist_input.rb +41 -0
- data/lib/formtastic/inputs/file_input.rb +2 -2
- data/lib/formtastic/namespaced_class_finder.rb +89 -0
- data/lib/formtastic/util.rb +1 -1
- data/lib/formtastic/version.rb +1 -1
- data/lib/generators/templates/formtastic.rb +20 -0
- data/spec/action_class_finder_spec.rb +12 -0
- data/spec/builder/custom_builder_spec.rb +2 -2
- data/spec/builder/semantic_fields_for_spec.rb +4 -4
- data/spec/helpers/action_helper_spec.rb +9 -355
- data/spec/helpers/form_helper_spec.rb +11 -1
- data/spec/helpers/input_helper_spec.rb +1 -916
- data/spec/helpers/namespaced_action_helper_spec.rb +43 -0
- data/spec/helpers/namespaced_input_helper_spec.rb +36 -0
- data/spec/input_class_finder_spec.rb +10 -0
- data/spec/inputs/check_boxes_input_spec.rb +2 -2
- data/spec/inputs/datalist_input_spec.rb +61 -0
- data/spec/inputs/select_input_spec.rb +1 -1
- data/spec/localizer_spec.rb +2 -2
- data/spec/namespaced_class_finder_spec.rb +79 -0
- data/spec/spec_helper.rb +17 -7
- data/spec/support/custom_macros.rb +22 -4
- data/spec/support/shared_examples.rb +1244 -0
- data/spec/support/specialized_class_finder_shared_example.rb +27 -0
- data/spec/support/test_environment.rb +1 -1
- data/spec/util_spec.rb +20 -6
- metadata +66 -15
- 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`
|
90
|
-
# * `:check_boxes`
|
91
|
-
# * `:color`
|
92
|
-
# * `:country`
|
93
|
-
# * `:datetime_select`
|
94
|
-
# * `:date_select`
|
95
|
-
# * `:email`
|
96
|
-
# * `:file`
|
97
|
-
# * `:hidden`
|
98
|
-
# * `:number`
|
99
|
-
# * `:password`
|
100
|
-
# * `:phone`
|
101
|
-
# * `:radio`
|
102
|
-
# * `:search`
|
103
|
-
# * `:select`
|
104
|
-
# * `:string`
|
105
|
-
# * `:text`
|
106
|
-
# * `:time_zone`
|
107
|
-
# * `:time_select`
|
108
|
-
# * `:url`
|
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
|
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
|
-
|
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
|
306
|
-
# `:as => :
|
307
|
-
#
|
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
|
-
#
|
310
|
-
#
|
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
|
data/lib/formtastic/inputs.rb
CHANGED
@@ -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
|
-
|
@@ -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.
|
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.
|
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="
|
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
|