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