simple_form 1.3.1 → 1.4.0
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.
Potentially problematic release.
This version of simple_form might be problematic. Click here for more details.
- data/.travis.yml +6 -0
- data/CHANGELOG.rdoc +24 -0
- data/Gemfile +3 -8
- data/Gemfile.lock +36 -36
- data/MIT-LICENSE +1 -1
- data/README.rdoc +70 -23
- data/lib/generators/simple_form/templates/_form.html.erb +1 -1
- data/lib/generators/simple_form/templates/_form.html.haml +2 -10
- data/lib/generators/simple_form/templates/_form.html.slim +10 -0
- data/lib/generators/simple_form/templates/simple_form.rb +20 -4
- data/lib/simple_form.rb +28 -1
- data/lib/simple_form/action_view_extensions/builder.rb +9 -10
- data/lib/simple_form/action_view_extensions/form_helper.rb +2 -1
- data/lib/simple_form/components/errors.rb +5 -1
- data/lib/simple_form/components/labels.rb +1 -1
- data/lib/simple_form/form_builder.rb +117 -29
- data/lib/simple_form/inputs/base.rb +42 -6
- data/lib/simple_form/inputs/collection_input.rb +0 -1
- data/lib/simple_form/inputs/mapping_input.rb +0 -6
- data/lib/simple_form/inputs/numeric_input.rb +15 -7
- data/lib/simple_form/inputs/string_input.rb +14 -3
- data/lib/simple_form/map_type.rb +1 -1
- data/lib/simple_form/version.rb +1 -1
- data/test/action_view_extensions/builder_test.rb +39 -7
- data/test/action_view_extensions/form_helper_test.rb +12 -0
- data/test/components/label_test.rb +44 -0
- data/test/discovery_inputs.rb +21 -0
- data/test/form_builder_test.rb +181 -0
- data/test/inputs_test.rb +150 -14
- data/test/support/misc_helpers.rb +12 -0
- data/test/support/models.rb +40 -3
- data/test/test_helper.rb +13 -4
- metadata +10 -6
@@ -39,8 +39,8 @@ module SimpleForm
|
|
39
39
|
render_collection(
|
40
40
|
attribute, collection, value_method, text_method, options, html_options
|
41
41
|
) do |value, text, default_html_options|
|
42
|
-
|
43
|
-
|
42
|
+
radio_button(attribute, value, default_html_options) +
|
43
|
+
label(sanitize_attribute_name(attribute, value), text, :class => "collection_radio")
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -83,8 +83,8 @@ module SimpleForm
|
|
83
83
|
) do |value, text, default_html_options|
|
84
84
|
default_html_options[:multiple] = true
|
85
85
|
|
86
|
-
check_box
|
87
|
-
|
86
|
+
check_box(attribute, default_html_options, value, '') +
|
87
|
+
label(sanitize_attribute_name(attribute, value), text, :class => "collection_check_boxes")
|
88
88
|
end
|
89
89
|
end
|
90
90
|
|
@@ -99,17 +99,16 @@ module SimpleForm
|
|
99
99
|
# end
|
100
100
|
def simple_fields_for(*args, &block)
|
101
101
|
options = args.extract_options!
|
102
|
-
|
102
|
+
if self.class < ActionView::Helpers::FormBuilder
|
103
|
+
options[:builder] ||= self.class
|
104
|
+
else
|
105
|
+
options[:builder] ||= SimpleForm::FormBuilder
|
106
|
+
end
|
103
107
|
fields_for(*(args << options), &block)
|
104
108
|
end
|
105
109
|
|
106
110
|
private
|
107
111
|
|
108
|
-
# Wraps the given component in a label, for better accessibility with collections.
|
109
|
-
def collection_label(attribute, value, component_tag, label_text, html_options) #:nodoc:
|
110
|
-
label(sanitize_attribute_name(attribute, value), component_tag << label_text.to_s, html_options)
|
111
|
-
end
|
112
|
-
|
113
112
|
# Generate default options for collection helpers, such as :checked and
|
114
113
|
# :disabled.
|
115
114
|
def default_html_options_for_collection(item, value, options, html_options) #:nodoc:
|
@@ -40,7 +40,8 @@ module SimpleForm
|
|
40
40
|
else dom_class(record_or_name_or_array)
|
41
41
|
end
|
42
42
|
options[:html] ||= {}
|
43
|
-
options[:html][:
|
43
|
+
options[:html][:novalidate] = !SimpleForm.browser_validations
|
44
|
+
options[:html][:class] = "\#{SimpleForm.form_class} \#{css_class} \#{options[:html][:class]}".strip
|
44
45
|
|
45
46
|
with_custom_field_error_proc do
|
46
47
|
#{helper}(record_or_name_or_array, *(args << options), &block)
|
@@ -36,7 +36,7 @@ module SimpleForm
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def label_html_options
|
39
|
-
label_options = html_options_for(:label, [input_type, required_class])
|
39
|
+
label_options = html_options_for(:label, [input_type, required_class, SimpleForm.label_class])
|
40
40
|
label_options[:for] = options[:input_html][:id] if options.key?(:input_html) && options[:input_html].key?(:id)
|
41
41
|
label_options
|
42
42
|
end
|
@@ -5,13 +5,17 @@ module SimpleForm
|
|
5
5
|
extend MapType
|
6
6
|
include SimpleForm::Inputs
|
7
7
|
|
8
|
-
map_type :
|
9
|
-
map_type :string, :email, :search, :tel, :url, :to => SimpleForm::Inputs::StringInput
|
10
|
-
map_type :integer, :decimal, :float,
|
11
|
-
map_type :select, :radio, :check_boxes,
|
12
|
-
map_type :date, :time, :datetime,
|
13
|
-
map_type :country, :time_zone,
|
14
|
-
map_type :boolean,
|
8
|
+
map_type :text, :file, :to => SimpleForm::Inputs::MappingInput
|
9
|
+
map_type :string, :password, :email, :search, :tel, :url, :to => SimpleForm::Inputs::StringInput
|
10
|
+
map_type :integer, :decimal, :float, :to => SimpleForm::Inputs::NumericInput
|
11
|
+
map_type :select, :radio, :check_boxes, :to => SimpleForm::Inputs::CollectionInput
|
12
|
+
map_type :date, :time, :datetime, :to => SimpleForm::Inputs::DateTimeInput
|
13
|
+
map_type :country, :time_zone, :to => SimpleForm::Inputs::PriorityInput
|
14
|
+
map_type :boolean, :to => SimpleForm::Inputs::BooleanInput
|
15
|
+
|
16
|
+
def self.discovery_cache
|
17
|
+
@discovery_cache ||= {}
|
18
|
+
end
|
15
19
|
|
16
20
|
# Basic input helper, combines all components in the stack to generate
|
17
21
|
# input html based on options the user define and some guesses through
|
@@ -85,12 +89,31 @@ module SimpleForm
|
|
85
89
|
if block_given?
|
86
90
|
SimpleForm::Inputs::BlockInput.new(self, attribute_name, column, input_type, options, &block).render
|
87
91
|
else
|
88
|
-
|
89
|
-
klass.new(self, attribute_name, column, input_type, options).render
|
92
|
+
find_mapping(input_type).new(self, attribute_name, column, input_type, options).render
|
90
93
|
end
|
91
94
|
end
|
92
95
|
alias :attribute :input
|
93
96
|
|
97
|
+
# Creates a input tag for the given attribute. All the given options
|
98
|
+
# are sent as :input_html.
|
99
|
+
#
|
100
|
+
# == Examples
|
101
|
+
#
|
102
|
+
# simple_form_for @user do |f|
|
103
|
+
# f.input_field :name
|
104
|
+
# end
|
105
|
+
#
|
106
|
+
# This is the output html (only the input portion, not the form):
|
107
|
+
#
|
108
|
+
# <input class="string required" id="user_name" maxlength="100"
|
109
|
+
# name="user[name]" size="100" type="text" value="Carlos" />
|
110
|
+
#
|
111
|
+
def input_field(attribute_name, options={})
|
112
|
+
options[:input_html] = options.except(:as)
|
113
|
+
options.merge!(:components => [:input], :wrapper => false)
|
114
|
+
input(attribute_name, options)
|
115
|
+
end
|
116
|
+
|
94
117
|
# Helper for dealing with association selects/radios, generating the
|
95
118
|
# collection automatically. It's just a wrapper to input, so all options
|
96
119
|
# supported in input are also supported by association. Some extra options
|
@@ -133,7 +156,7 @@ module SimpleForm
|
|
133
156
|
when :belongs_to
|
134
157
|
reflection.options[:foreign_key] || :"#{reflection.name}_id"
|
135
158
|
when :has_one
|
136
|
-
raise ":has_one
|
159
|
+
raise ":has_one associations are not supported by f.association"
|
137
160
|
else
|
138
161
|
if options[:as] == :select
|
139
162
|
html_options = options[:input_html] ||= {}
|
@@ -141,6 +164,12 @@ module SimpleForm
|
|
141
164
|
html_options[:multiple] = true unless html_options.key?(:multiple)
|
142
165
|
end
|
143
166
|
|
167
|
+
# Force the association to be preloaded for performance.
|
168
|
+
if options[:preload] != false && object.respond_to?(association)
|
169
|
+
target = object.send(association)
|
170
|
+
target.to_a if target.respond_to?(:to_a)
|
171
|
+
end
|
172
|
+
|
144
173
|
:"#{reflection.name.to_s.singularize}_ids"
|
145
174
|
end
|
146
175
|
|
@@ -181,6 +210,23 @@ module SimpleForm
|
|
181
210
|
SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options).error
|
182
211
|
end
|
183
212
|
|
213
|
+
# Return the error but also considering its name. This is used
|
214
|
+
# when errors for a hidden field need to be shown.
|
215
|
+
#
|
216
|
+
# == Examples
|
217
|
+
#
|
218
|
+
# f.full_error :token #=> <span class="error">Token is invalid</span>
|
219
|
+
#
|
220
|
+
def full_error(attribute_name, options={})
|
221
|
+
options[:error_prefix] ||= if object.class.respond_to?(:human_attribute_name)
|
222
|
+
object.class.human_attribute_name(attribute_name.to_s)
|
223
|
+
else
|
224
|
+
attribute_name.to_s.humanize
|
225
|
+
end
|
226
|
+
|
227
|
+
error(attribute_name, options)
|
228
|
+
end
|
229
|
+
|
184
230
|
# Creates a hint tag for the given attribute. Accepts a symbol indicating
|
185
231
|
# an attribute for I18n lookup or a string. All the given options are sent
|
186
232
|
# as :hint_html.
|
@@ -254,48 +300,90 @@ module SimpleForm
|
|
254
300
|
|
255
301
|
input_type = column.try(:type)
|
256
302
|
case input_type
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
end
|
268
|
-
|
269
|
-
match || input_type || file_method?(attribute_name) || :string
|
303
|
+
when :timestamp
|
304
|
+
:datetime
|
305
|
+
when :string, nil
|
306
|
+
case attribute_name.to_s
|
307
|
+
when /password/ then :password
|
308
|
+
when /time_zone/ then :time_zone
|
309
|
+
when /country/ then :country
|
310
|
+
when /email/ then :email
|
311
|
+
when /phone/ then :tel
|
312
|
+
when /url/ then :url
|
270
313
|
else
|
271
|
-
input_type
|
314
|
+
file_method?(attribute_name) ? :file : (input_type || :string)
|
315
|
+
end
|
316
|
+
else
|
317
|
+
input_type
|
272
318
|
end
|
273
319
|
end
|
274
320
|
|
275
|
-
def find_custom_type(attribute_name)
|
321
|
+
def find_custom_type(attribute_name) #:nodoc:
|
276
322
|
SimpleForm.input_mappings.find { |match, type|
|
277
323
|
attribute_name =~ match
|
278
324
|
}.try(:last) if SimpleForm.input_mappings
|
279
325
|
end
|
280
326
|
|
281
|
-
# Checks if attribute is a file_method.
|
282
327
|
def file_method?(attribute_name) #:nodoc:
|
283
328
|
file = @object.send(attribute_name) if @object.respond_to?(attribute_name)
|
284
|
-
|
329
|
+
file && SimpleForm.file_methods.any? { |m| file.respond_to?(m) }
|
285
330
|
end
|
286
331
|
|
287
|
-
# Finds the database column for the given attribute.
|
288
332
|
def find_attribute_column(attribute_name) #:nodoc:
|
289
333
|
if @object.respond_to?(:column_for_attribute)
|
290
334
|
@object.column_for_attribute(attribute_name)
|
291
335
|
end
|
292
336
|
end
|
293
337
|
|
294
|
-
# Find reflection related to association.
|
295
338
|
def find_association_reflection(association) #:nodoc:
|
296
339
|
if @object.class.respond_to?(:reflect_on_association)
|
297
340
|
@object.class.reflect_on_association(association)
|
298
341
|
end
|
299
342
|
end
|
343
|
+
|
344
|
+
# Attempts to find a mapping. It follows the following rules:
|
345
|
+
#
|
346
|
+
# 1) It tries to find a registered mapping, if succeeds:
|
347
|
+
# a) Try to find an alternative with the same name in the Object scope
|
348
|
+
# b) Or use the found mapping
|
349
|
+
# 2) If not, fallbacks to #{input_type}Input
|
350
|
+
# 3) If not, fallbacks to SimpleForm::Inputs::#{input_type}Input
|
351
|
+
def find_mapping(input_type) #:nodoc:
|
352
|
+
discovery_cache[input_type] ||=
|
353
|
+
if mapping = self.class.mappings[input_type]
|
354
|
+
mapping_override(mapping) || mapping
|
355
|
+
else
|
356
|
+
camelized = "#{input_type.to_s.camelize}Input"
|
357
|
+
attempt_mapping(camelized, Object) || attempt_mapping(camelized, self.class) ||
|
358
|
+
raise("No input found for #{input_type}")
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# If cache_discovery is enabled, use the class level cache that persists
|
363
|
+
# between requests, otherwise use the instance one.
|
364
|
+
def discovery_cache #:nodoc:
|
365
|
+
if SimpleForm.cache_discovery
|
366
|
+
self.class.discovery_cache
|
367
|
+
else
|
368
|
+
@discovery_cache ||= {}
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def mapping_override(klass) #:nodoc:
|
373
|
+
name = klass.name
|
374
|
+
if name =~ /^SimpleForm::Inputs/
|
375
|
+
attempt_mapping name.split("::").last, Object
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def attempt_mapping(mapping, at) #:nodoc:
|
380
|
+
return if SimpleForm.inputs_discovery == false && at == Object
|
381
|
+
|
382
|
+
begin
|
383
|
+
at.const_get(mapping)
|
384
|
+
rescue NameError => e
|
385
|
+
e.message =~ /#{mapping}$/ ? nil : raise
|
386
|
+
end
|
387
|
+
end
|
300
388
|
end
|
301
389
|
end
|
@@ -28,9 +28,9 @@ module SimpleForm
|
|
28
28
|
@reflection = options.delete(:reflection)
|
29
29
|
@options = options
|
30
30
|
@input_html_options = html_options_for(:input, input_html_classes).tap do |o|
|
31
|
-
o[:required] = true if has_required?
|
31
|
+
o[:required] = true if has_required? # Don't make this conditional on HTML5 here, because we want the CSS class to be set
|
32
32
|
o[:disabled] = true if disabled?
|
33
|
-
o[:autofocus] = true if has_autofocus?
|
33
|
+
o[:autofocus] = true if has_autofocus? && SimpleForm.html5
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
@@ -74,7 +74,7 @@ module SimpleForm
|
|
74
74
|
|
75
75
|
# Whether this input is valid for HTML 5 required attribute.
|
76
76
|
def has_required?
|
77
|
-
attribute_required?
|
77
|
+
attribute_required? && SimpleForm.html5
|
78
78
|
end
|
79
79
|
|
80
80
|
def has_autofocus?
|
@@ -130,6 +130,15 @@ module SimpleForm
|
|
130
130
|
# Action is the action being rendered, usually :new or :edit.
|
131
131
|
# And attribute is the attribute itself, :name for example.
|
132
132
|
#
|
133
|
+
# The lookup for nested attributes is also done in a nested format using
|
134
|
+
# both model and nested object names, such as follow:
|
135
|
+
#
|
136
|
+
# simple_form.{namespace}.{model}.{nested}.{action}.{attribute}
|
137
|
+
# simple_form.{namespace}.{model}.{nested}.{attribute}
|
138
|
+
# simple_form.{namespace}.{nested}.{action}.{attribute}
|
139
|
+
# simple_form.{namespace}.{nested}.{attribute}
|
140
|
+
# simple_form.{namespace}.{attribute}
|
141
|
+
#
|
133
142
|
# Example:
|
134
143
|
#
|
135
144
|
# simple_form:
|
@@ -143,14 +152,36 @@ module SimpleForm
|
|
143
152
|
# Take a look at our locale example file.
|
144
153
|
def translate(namespace, default='')
|
145
154
|
return nil unless SimpleForm.translate
|
146
|
-
|
147
|
-
|
148
|
-
lookups
|
155
|
+
|
156
|
+
model_names = lookup_model_names
|
157
|
+
lookups = []
|
158
|
+
|
159
|
+
while !model_names.empty?
|
160
|
+
joined_model_names = model_names.join(".")
|
161
|
+
model_names.shift
|
162
|
+
|
163
|
+
lookups << :"#{joined_model_names}.#{lookup_action}.#{reflection_or_attribute_name}"
|
164
|
+
lookups << :"#{joined_model_names}.#{reflection_or_attribute_name}"
|
165
|
+
end
|
149
166
|
lookups << :"#{reflection_or_attribute_name}"
|
150
167
|
lookups << default
|
168
|
+
|
151
169
|
I18n.t(lookups.shift, :scope => :"simple_form.#{namespace}", :default => lookups).presence
|
152
170
|
end
|
153
171
|
|
172
|
+
# Extract the model names from the object_name mess.
|
173
|
+
#
|
174
|
+
# Example:
|
175
|
+
#
|
176
|
+
# route[blocks_attributes][0][blocks_learning_object_attributes][1][foo_attributes]
|
177
|
+
# ["route", "blocks", "blocks_learning_object", "foo"]
|
178
|
+
#
|
179
|
+
def lookup_model_names
|
180
|
+
object_name.to_s.scan(/([a-zA-Z_]+)/).flatten.map do |x|
|
181
|
+
x.gsub('_attributes', '')
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
154
185
|
# The action to be used in lookup.
|
155
186
|
def lookup_action
|
156
187
|
action = template.controller.action_name
|
@@ -158,6 +189,11 @@ module SimpleForm
|
|
158
189
|
action = action.to_sym
|
159
190
|
ACTIONS[action] || action
|
160
191
|
end
|
192
|
+
|
193
|
+
def input_method
|
194
|
+
self.class.mappings[input_type] or
|
195
|
+
raise("Could not find method for #{input_type.inspect}")
|
196
|
+
end
|
161
197
|
end
|
162
198
|
end
|
163
199
|
end
|
@@ -4,7 +4,6 @@ module SimpleForm
|
|
4
4
|
class MappingInput < Base
|
5
5
|
extend MapType
|
6
6
|
|
7
|
-
map_type :password, :to => :password_field
|
8
7
|
map_type :text, :to => :text_area
|
9
8
|
map_type :file, :to => :file_field
|
10
9
|
|
@@ -14,11 +13,6 @@ module SimpleForm
|
|
14
13
|
|
15
14
|
private
|
16
15
|
|
17
|
-
def input_method
|
18
|
-
self.class.mappings[input_type] or
|
19
|
-
raise("Could not find method for #{input_type.inspect}")
|
20
|
-
end
|
21
|
-
|
22
16
|
def has_placeholder?
|
23
17
|
(text? || password?) && placeholder_present?
|
24
18
|
end
|
@@ -2,10 +2,10 @@ module SimpleForm
|
|
2
2
|
module Inputs
|
3
3
|
class NumericInput < Base
|
4
4
|
def input
|
5
|
-
input_html_options[:type] ||= "number"
|
5
|
+
input_html_options[:type] ||= "number" if SimpleForm.html5
|
6
6
|
input_html_options[:size] ||= SimpleForm.default_input_size
|
7
|
-
input_html_options[:step] ||= 1 if
|
8
|
-
infer_attributes_from_validations!
|
7
|
+
input_html_options[:step] ||= integer? ? 1 : "any" if SimpleForm.html5
|
8
|
+
infer_attributes_from_validations! if SimpleForm.html5
|
9
9
|
@builder.text_field(attribute_name, input_html_options)
|
10
10
|
end
|
11
11
|
|
@@ -35,23 +35,31 @@ module SimpleForm
|
|
35
35
|
|
36
36
|
def minimum_value(validator_options)
|
37
37
|
if integer? && validator_options.key?(:greater_than)
|
38
|
-
validator_options[:greater_than] + 1
|
38
|
+
evaluate_validator_option(validator_options[:greater_than]) + 1
|
39
39
|
else
|
40
|
-
validator_options[:greater_than_or_equal_to]
|
40
|
+
evaluate_validator_option(validator_options[:greater_than_or_equal_to])
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
44
|
def maximum_value(validator_options)
|
45
45
|
if integer? && validator_options.key?(:less_than)
|
46
|
-
validator_options[:less_than] - 1
|
46
|
+
evaluate_validator_option(validator_options[:less_than]) - 1
|
47
47
|
else
|
48
|
-
validator_options[:less_than_or_equal_to]
|
48
|
+
evaluate_validator_option(validator_options[:less_than_or_equal_to])
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
52
|
def find_numericality_validator
|
53
53
|
attribute_validators.find { |v| ActiveModel::Validations::NumericalityValidator === v }
|
54
54
|
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def evaluate_validator_option(option)
|
59
|
+
return option if option.is_a?(Numeric)
|
60
|
+
return object.send(option) if option.is_a?(Symbol)
|
61
|
+
return option.call(object) if option.respond_to?(:call)
|
62
|
+
end
|
55
63
|
end
|
56
64
|
end
|
57
65
|
end
|