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.

@@ -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
- radio = radio_button(attribute, value, default_html_options)
43
- collection_label(attribute, value, radio, text, :class => "collection_radio")
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 = check_box(attribute, default_html_options, value, '')
87
- collection_label(attribute, value, check_box, text, :class => "collection_check_boxes")
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
- options[:builder] = SimpleForm::FormBuilder
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][:class] = "simple_form \#{css_class} \#{options[:html][:class]}".strip
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)
@@ -12,7 +12,11 @@ module SimpleForm
12
12
  end
13
13
 
14
14
  def error_text
15
- errors.send(error_method)
15
+ if options[:error_prefix]
16
+ options[:error_prefix] + " " + errors.send(error_method)
17
+ else
18
+ errors.send(error_method)
19
+ end
16
20
  end
17
21
 
18
22
  def error_method
@@ -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 :password, :text, :file, :to => SimpleForm::Inputs::MappingInput
9
- map_type :string, :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
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
- klass = self.class.mappings[input_type] || self.class.const_get("#{input_type.to_s.camelize}Input")
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 association are not supported by f.association"
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
- when :timestamp
258
- :datetime
259
- when :string, nil
260
- match = case attribute_name.to_s
261
- when /password/ then :password
262
- when /time_zone/ then :time_zone
263
- when /country/ then :country
264
- when /email/ then :email
265
- when /phone/ then :tel
266
- when /url/ then :url
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
- :file if file && SimpleForm.file_methods.any? { |m| file.respond_to?(m) }
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
- lookups = []
147
- lookups << :"#{object_name}.#{lookup_action}.#{reflection_or_attribute_name}"
148
- lookups << :"#{object_name}.#{reflection_or_attribute_name}"
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
@@ -14,7 +14,6 @@ module SimpleForm
14
14
 
15
15
  def input
16
16
  label_method, value_method = detect_collection_methods
17
-
18
17
  @builder.send(:"collection_#{input_type}", attribute_name, collection,
19
18
  value_method, label_method, input_options, input_html_options)
20
19
  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 integer?
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