formtastic 2.1.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +1 -0
- data/.github/workflows/test.yml +61 -0
- data/.gitignore +4 -2
- data/CHANGELOG.md +52 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +105 -0
- data/MIT-LICENSE +1 -1
- data/{README.textile → README.md} +204 -219
- data/RELEASE_PROCESS +3 -1
- data/Rakefile +27 -29
- data/app/assets/stylesheets/formtastic.css +3 -2
- data/bin/appraisal +8 -0
- data/formtastic.gemspec +11 -14
- data/gemfiles/rails_5.2/Gemfile +5 -0
- data/gemfiles/rails_6.0/Gemfile +5 -0
- data/gemfiles/rails_6.1/Gemfile +5 -0
- data/gemfiles/rails_edge/Gemfile +13 -0
- data/lib/formtastic/action_class_finder.rb +18 -0
- data/lib/formtastic/actions/button_action.rb +55 -60
- data/lib/formtastic/actions/input_action.rb +59 -57
- data/lib/formtastic/actions/link_action.rb +68 -67
- data/lib/formtastic/actions.rb +6 -3
- data/lib/formtastic/deprecation.rb +5 -0
- data/lib/formtastic/engine.rb +3 -1
- data/lib/formtastic/form_builder.rb +35 -16
- data/lib/formtastic/helpers/action_helper.rb +34 -28
- data/lib/formtastic/helpers/enum.rb +13 -0
- data/lib/formtastic/helpers/errors_helper.rb +2 -2
- data/lib/formtastic/helpers/fieldset_wrapper.rb +16 -12
- data/lib/formtastic/helpers/form_helper.rb +19 -16
- data/lib/formtastic/helpers/input_helper.rb +69 -97
- data/lib/formtastic/helpers/inputs_helper.rb +35 -25
- data/lib/formtastic/helpers/reflection.rb +4 -4
- data/lib/formtastic/helpers.rb +1 -2
- data/lib/formtastic/html_attributes.rb +12 -1
- data/lib/formtastic/i18n.rb +1 -1
- data/lib/formtastic/input_class_finder.rb +18 -0
- data/lib/formtastic/inputs/base/choices.rb +2 -2
- data/lib/formtastic/inputs/base/collections.rb +46 -14
- data/lib/formtastic/inputs/base/database.rb +7 -2
- data/lib/formtastic/inputs/base/datetime_pickerish.rb +85 -0
- data/lib/formtastic/inputs/base/errors.rb +7 -7
- data/lib/formtastic/inputs/base/hints.rb +2 -2
- data/lib/formtastic/inputs/base/html.rb +10 -9
- data/lib/formtastic/inputs/base/labelling.rb +5 -8
- data/lib/formtastic/inputs/base/naming.rb +4 -4
- data/lib/formtastic/inputs/base/numeric.rb +1 -1
- data/lib/formtastic/inputs/base/options.rb +3 -4
- data/lib/formtastic/inputs/base/stringish.rb +10 -2
- data/lib/formtastic/inputs/base/timeish.rb +34 -22
- data/lib/formtastic/inputs/base/validations.rb +41 -13
- data/lib/formtastic/inputs/base/wrapping.rb +29 -26
- data/lib/formtastic/inputs/base.rb +22 -15
- data/lib/formtastic/inputs/boolean_input.rb +26 -12
- data/lib/formtastic/inputs/check_boxes_input.rb +39 -31
- data/lib/formtastic/inputs/color_input.rb +41 -0
- data/lib/formtastic/inputs/country_input.rb +24 -5
- data/lib/formtastic/inputs/datalist_input.rb +41 -0
- data/lib/formtastic/inputs/date_picker_input.rb +93 -0
- data/lib/formtastic/inputs/{date_input.rb → date_select_input.rb} +1 -1
- data/lib/formtastic/inputs/datetime_picker_input.rb +103 -0
- data/lib/formtastic/inputs/{datetime_input.rb → datetime_select_input.rb} +1 -1
- data/lib/formtastic/inputs/file_input.rb +2 -2
- data/lib/formtastic/inputs/hidden_input.rb +2 -6
- data/lib/formtastic/inputs/radio_input.rb +28 -22
- data/lib/formtastic/inputs/select_input.rb +36 -39
- data/lib/formtastic/inputs/time_picker_input.rb +99 -0
- data/lib/formtastic/inputs/{time_input.rb → time_select_input.rb} +6 -2
- data/lib/formtastic/inputs/time_zone_input.rb +16 -6
- data/lib/formtastic/inputs.rb +32 -21
- data/lib/formtastic/localized_string.rb +1 -1
- data/lib/formtastic/localizer.rb +24 -24
- data/lib/formtastic/namespaced_class_finder.rb +99 -0
- data/lib/formtastic/version.rb +1 -1
- data/lib/formtastic.rb +20 -10
- data/lib/generators/formtastic/form/form_generator.rb +10 -4
- data/lib/generators/formtastic/input/input_generator.rb +46 -0
- data/lib/generators/formtastic/install/install_generator.rb +5 -19
- data/lib/generators/templates/_form.html.slim +2 -2
- data/lib/generators/templates/formtastic.rb +46 -25
- data/lib/generators/templates/input.rb +19 -0
- data/sample/basic_inputs.html +23 -3
- data/script/integration-template.rb +74 -0
- data/script/integration.sh +19 -0
- data/spec/action_class_finder_spec.rb +12 -0
- data/spec/actions/button_action_spec.rb +8 -8
- data/spec/actions/generic_action_spec.rb +92 -56
- data/spec/actions/input_action_spec.rb +7 -7
- data/spec/actions/link_action_spec.rb +10 -10
- data/spec/builder/custom_builder_spec.rb +36 -20
- data/spec/builder/error_proc_spec.rb +4 -4
- data/spec/builder/semantic_fields_for_spec.rb +28 -29
- data/spec/fast_spec_helper.rb +12 -0
- data/spec/generators/formtastic/form/form_generator_spec.rb +45 -32
- data/spec/generators/formtastic/input/input_generator_spec.rb +124 -0
- data/spec/generators/formtastic/install/install_generator_spec.rb +9 -9
- data/spec/helpers/action_helper_spec.rb +75 -103
- data/spec/helpers/actions_helper_spec.rb +17 -17
- data/spec/helpers/form_helper_spec.rb +84 -33
- data/spec/helpers/input_helper_spec.rb +333 -285
- data/spec/helpers/inputs_helper_spec.rb +167 -121
- data/spec/helpers/reflection_helper_spec.rb +3 -3
- data/spec/helpers/semantic_errors_helper_spec.rb +23 -23
- data/spec/i18n_spec.rb +26 -26
- data/spec/input_class_finder_spec.rb +10 -0
- data/spec/inputs/base/collections_spec.rb +76 -0
- data/spec/inputs/base/validations_spec.rb +480 -0
- data/spec/inputs/boolean_input_spec.rb +100 -65
- data/spec/inputs/check_boxes_input_spec.rb +200 -101
- data/spec/inputs/color_input_spec.rb +85 -0
- data/spec/inputs/country_input_spec.rb +20 -20
- data/spec/inputs/custom_input_spec.rb +3 -4
- data/spec/inputs/datalist_input_spec.rb +61 -0
- data/spec/inputs/date_picker_input_spec.rb +449 -0
- data/spec/inputs/date_select_input_spec.rb +249 -0
- data/spec/inputs/datetime_picker_input_spec.rb +490 -0
- data/spec/inputs/datetime_select_input_spec.rb +209 -0
- data/spec/inputs/email_input_spec.rb +5 -5
- data/spec/inputs/file_input_spec.rb +6 -6
- data/spec/inputs/hidden_input_spec.rb +22 -35
- data/spec/inputs/include_blank_spec.rb +11 -11
- data/spec/inputs/label_spec.rb +62 -25
- data/spec/inputs/number_input_spec.rb +112 -112
- data/spec/inputs/password_input_spec.rb +5 -5
- data/spec/inputs/phone_input_spec.rb +5 -5
- data/spec/inputs/placeholder_spec.rb +6 -6
- data/spec/inputs/radio_input_spec.rb +99 -55
- data/spec/inputs/range_input_spec.rb +66 -66
- data/spec/inputs/readonly_spec.rb +50 -0
- data/spec/inputs/search_input_spec.rb +5 -5
- data/spec/inputs/select_input_spec.rb +170 -170
- data/spec/inputs/string_input_spec.rb +68 -16
- data/spec/inputs/text_input_spec.rb +16 -16
- data/spec/inputs/time_picker_input_spec.rb +455 -0
- data/spec/inputs/time_select_input_spec.rb +261 -0
- data/spec/inputs/time_zone_input_spec.rb +54 -28
- data/spec/inputs/url_input_spec.rb +5 -5
- data/spec/inputs/with_options_spec.rb +7 -7
- data/spec/localizer_spec.rb +39 -17
- data/spec/namespaced_class_finder_spec.rb +79 -0
- data/spec/schema.rb +21 -0
- data/spec/spec_helper.rb +254 -221
- data/spec/support/custom_macros.rb +128 -95
- data/spec/support/shared_examples.rb +12 -0
- data/spec/support/specialized_class_finder_shared_example.rb +27 -0
- data/spec/support/test_environment.rb +26 -10
- metadata +177 -238
- data/.travis.yml +0 -8
- data/Appraisals +0 -11
- data/CHANGELOG +0 -371
- data/gemfiles/rails-3.0.gemfile +0 -7
- data/gemfiles/rails-3.1.gemfile +0 -7
- data/gemfiles/rails-3.2.gemfile +0 -7
- data/lib/formtastic/helpers/buttons_helper.rb +0 -310
- data/lib/formtastic/inputs/base/grouped_collections.rb +0 -77
- data/lib/formtastic/util.rb +0 -25
- data/lib/tasks/verify_rcov.rb +0 -44
- data/spec/helpers/buttons_helper_spec.rb +0 -166
- data/spec/helpers/commit_button_helper_spec.rb +0 -530
- data/spec/inputs/date_input_spec.rb +0 -227
- data/spec/inputs/datetime_input_spec.rb +0 -185
- data/spec/inputs/time_input_spec.rb +0 -267
- 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
|
-
|
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}=",
|
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 {
|
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
|
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
|
-
|
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 =
|
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 = {})
|
89
|
+
def default_action_type(method, options = {}) # @private
|
75
90
|
case method
|
76
91
|
when :submit then :input
|
77
|
-
when :reset
|
92
|
+
when :reset then :input
|
78
93
|
when :cancel then :link
|
94
|
+
else method
|
79
95
|
end
|
80
96
|
end
|
81
97
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
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)
|
25
|
-
contents = args.
|
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
|
-
|
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
|
-
|
51
|
-
legend
|
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)
|
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?(
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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
|
-
|
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 =
|
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`
|
90
|
-
# * `:check_boxes`
|
91
|
-
# * `:
|
92
|
-
# * `:
|
93
|
-
# * `:
|
94
|
-
# * `:
|
95
|
-
# * `:
|
96
|
-
# * `:
|
97
|
-
# * `:
|
98
|
-
# * `:
|
99
|
-
# * `:
|
100
|
-
# * `:
|
101
|
-
# * `:
|
102
|
-
# * `:
|
103
|
-
# * `:
|
104
|
-
# * `:
|
105
|
-
# * `:
|
106
|
-
# * `:
|
107
|
-
# * `:
|
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 `:
|
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 =
|
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 = {})
|
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
|
-
|
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 :
|
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
|
-
|
313
|
-
|
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
|
317
|
-
# `:as => :
|
318
|
-
#
|
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
|
-
#
|
321
|
-
#
|
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
|
-
|
335
|
-
|
336
|
-
@
|
337
|
-
|
338
|
-
|
339
|
-
|
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
|