simple_form 2.0.0 → 2.1.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.
Files changed (60) hide show
  1. data/CHANGELOG.md +34 -231
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +221 -159
  4. data/lib/generators/simple_form/install_generator.rb +14 -5
  5. data/lib/generators/simple_form/templates/config/initializers/{simple_form.rb.tt → simple_form.rb} +8 -42
  6. data/lib/generators/simple_form/templates/config/initializers/simple_form_bootstrap.rb +45 -0
  7. data/lib/generators/simple_form/templates/config/initializers/simple_form_foundation.rb +26 -0
  8. data/lib/generators/simple_form/templates/config/locales/simple_form.en.yml +7 -5
  9. data/lib/simple_form/action_view_extensions/builder.rb +46 -30
  10. data/lib/simple_form/action_view_extensions/builder.rb.orig +247 -0
  11. data/lib/simple_form/action_view_extensions/form_helper.rb +7 -9
  12. data/lib/simple_form/components/hints.rb +3 -2
  13. data/lib/simple_form/components/label_input.rb +1 -1
  14. data/lib/simple_form/components/labels.rb +1 -1
  15. data/lib/simple_form/components/maxlength.rb +1 -1
  16. data/lib/simple_form/components/min_max.rb +2 -1
  17. data/lib/simple_form/components/pattern.rb +1 -1
  18. data/lib/simple_form/components/readonly.rb +1 -1
  19. data/lib/simple_form/components.rb +12 -10
  20. data/lib/simple_form/error_notification.rb +1 -1
  21. data/lib/simple_form/form_builder.rb +18 -8
  22. data/lib/simple_form/form_builder.rb.orig +486 -0
  23. data/lib/simple_form/helpers/validators.rb +2 -2
  24. data/lib/simple_form/inputs/base.rb +33 -8
  25. data/lib/simple_form/inputs/boolean_input.rb +19 -5
  26. data/lib/simple_form/inputs/collection_radio_buttons_input.rb +2 -6
  27. data/lib/simple_form/inputs/date_time_input.rb +0 -4
  28. data/lib/simple_form/inputs.rb +19 -17
  29. data/lib/simple_form/version.rb +1 -1
  30. data/lib/simple_form/version.rb.orig +7 -0
  31. data/lib/simple_form/wrappers/many.rb +5 -1
  32. data/lib/simple_form/wrappers/root.rb +4 -2
  33. data/lib/simple_form/wrappers/single.rb +6 -0
  34. data/lib/simple_form.rb +17 -8
  35. data/test/action_view_extensions/builder_test.rb +145 -66
  36. data/test/action_view_extensions/form_helper_test.rb +59 -20
  37. data/test/components/label_test.rb +34 -17
  38. data/test/form_builder/association_test.rb +19 -10
  39. data/test/form_builder/error_notification_test.rb +1 -1
  40. data/test/form_builder/general_test.rb +74 -12
  41. data/test/form_builder/hint_test.rb +15 -0
  42. data/test/form_builder/input_field_test.rb +45 -0
  43. data/test/form_builder/label_test.rb +6 -0
  44. data/test/form_builder/wrapper_test.rb +58 -4
  45. data/test/generators/simple_form_generator_test.rb +11 -1
  46. data/test/inputs/boolean_input_test.rb +39 -0
  47. data/test/inputs/datetime_input_test.rb +2 -2
  48. data/test/inputs/disabled_test.rb +51 -11
  49. data/test/inputs/discovery_test.rb +9 -1
  50. data/test/inputs/general_test.rb +58 -11
  51. data/test/inputs/grouped_collection_select_input_test.rb +15 -0
  52. data/test/inputs/numeric_input_test.rb +6 -0
  53. data/test/inputs/readonly_test.rb +51 -11
  54. data/test/inputs/string_input_test.rb +6 -0
  55. data/test/support/discovery_inputs.rb +6 -0
  56. data/test/support/misc_helpers.rb +41 -1
  57. data/test/support/models.rb +8 -4
  58. data/test/test_helper.rb +8 -6
  59. metadata +26 -13
  60. data/test/support/mock_response.rb +0 -14
@@ -1,4 +1,5 @@
1
1
  require 'simple_form/core_ext/hash'
2
+ require 'simple_form/map_type'
2
3
 
3
4
  module SimpleForm
4
5
  class FormBuilder < ActionView::Helpers::FormBuilder
@@ -100,19 +101,20 @@ module SimpleForm
100
101
  # == Priority
101
102
  #
102
103
  # Some inputs, as :time_zone and :country accepts a :priority option. If none is
103
- # given SimpleForm.time_zone_priority and SimpleForm.country_priority are used respectivelly.
104
+ # given SimpleForm.time_zone_priority and SimpleForm.country_priority are used respectively.
104
105
  #
105
106
  def input(attribute_name, options={}, &block)
106
107
  options = @defaults.deep_dup.deep_merge(options) if @defaults
108
+ input = find_input(attribute_name, options, &block)
107
109
 
108
110
  chosen =
109
- if name = options[:wrapper]
111
+ if name = options[:wrapper] || find_wrapper_mapping(input.input_type)
110
112
  name.respond_to?(:render) ? name : SimpleForm.wrapper(name)
111
113
  else
112
114
  wrapper
113
115
  end
114
116
 
115
- chosen.render find_input(attribute_name, options, &block)
117
+ chosen.render input
116
118
  end
117
119
  alias :attribute :input
118
120
 
@@ -133,7 +135,9 @@ module SimpleForm
133
135
  def input_field(attribute_name, options={})
134
136
  options = options.dup
135
137
  options[:input_html] = options.except(:as, :collection, :label_method, :value_method)
136
- SimpleForm::Wrappers::Root.new([:input], :wrapper => false).render find_input(attribute_name, options)
138
+ options = @defaults.deep_dup.deep_merge(options) if @defaults
139
+
140
+ SimpleForm::Wrappers::Root.new([:min_max, :maxlength, :placeholder, :pattern, :readonly, :input], :wrapper => false).render find_input(attribute_name, options)
137
141
  end
138
142
 
139
143
  # Helper for dealing with association selects/radios, generating the
@@ -174,13 +178,15 @@ module SimpleForm
174
178
  raise "Association #{association.inspect} not found" unless reflection
175
179
 
176
180
  options[:as] ||= :select
177
- options[:collection] ||= reflection.klass.all(reflection.options.slice(:conditions, :order))
181
+ options[:collection] ||= options.fetch(:collection) {
182
+ reflection.klass.all(reflection.options.slice(:conditions, :order))
183
+ }
178
184
 
179
185
  attribute = case reflection.macro
180
186
  when :belongs_to
181
187
  (reflection.respond_to?(:options) && reflection.options[:foreign_key]) || :"#{reflection.name}_id"
182
188
  when :has_one
183
- raise ":has_one associations are not supported by f.association"
189
+ raise ArgumentError, ":has_one associations are not supported by f.association"
184
190
  else
185
191
  if options[:as] == :select
186
192
  html_options = options[:input_html] ||= {}
@@ -303,7 +309,7 @@ module SimpleForm
303
309
  return super if args.first.is_a?(String) || block_given?
304
310
 
305
311
  options = args.extract_options!.dup
306
- options[:label_html] = options.except(:label, :required)
312
+ options[:label_html] = options.except(:label, :required, :as)
307
313
 
308
314
  column = find_attribute_column(attribute_name)
309
315
  input_type = default_input_type(attribute_name, column, options)
@@ -442,6 +448,10 @@ module SimpleForm
442
448
  end
443
449
  end
444
450
 
451
+ def find_wrapper_mapping(input_type) #:nodoc:
452
+ SimpleForm.wrapper_mappings && SimpleForm.wrapper_mappings[input_type]
453
+ end
454
+
445
455
  # If cache_discovery is enabled, use the class level cache that persists
446
456
  # between requests, otherwise use the instance one.
447
457
  def discovery_cache #:nodoc:
@@ -465,7 +475,7 @@ module SimpleForm
465
475
  begin
466
476
  at.const_get(mapping)
467
477
  rescue NameError => e
468
- e.message =~ /#{mapping}$/ ? nil : raise
478
+ raise if e.message !~ /#{mapping}$/
469
479
  end
470
480
  end
471
481
  end
@@ -0,0 +1,486 @@
1
+ <<<<<<< HEAD
2
+ require 'simple_form/core_ext/hash'
3
+ =======
4
+ require 'active_support/core_ext/object/deep_dup'
5
+ require 'simple_form/map_type'
6
+ >>>>>>> beeac4d... These modules don't need to be autoloaded
7
+
8
+ module SimpleForm
9
+ class FormBuilder < ActionView::Helpers::FormBuilder
10
+ attr_reader :template, :object_name, :object, :wrapper
11
+
12
+ # When action is create or update, we still should use new and edit
13
+ ACTIONS = {
14
+ :create => :new,
15
+ :update => :edit
16
+ }
17
+
18
+ extend MapType
19
+ include SimpleForm::Inputs
20
+
21
+ map_type :text, :to => SimpleForm::Inputs::TextInput
22
+ map_type :file, :to => SimpleForm::Inputs::FileInput
23
+ map_type :string, :email, :search, :tel, :url, :to => SimpleForm::Inputs::StringInput
24
+ map_type :password, :to => SimpleForm::Inputs::PasswordInput
25
+ map_type :integer, :decimal, :float, :to => SimpleForm::Inputs::NumericInput
26
+ map_type :range, :to => SimpleForm::Inputs::RangeInput
27
+ map_type :check_boxes, :to => SimpleForm::Inputs::CollectionCheckBoxesInput
28
+ map_type :radio_buttons, :to => SimpleForm::Inputs::CollectionRadioButtonsInput
29
+ map_type :select, :to => SimpleForm::Inputs::CollectionSelectInput
30
+ map_type :grouped_select, :to => SimpleForm::Inputs::GroupedCollectionSelectInput
31
+ map_type :date, :time, :datetime, :to => SimpleForm::Inputs::DateTimeInput
32
+ map_type :country, :time_zone, :to => SimpleForm::Inputs::PriorityInput
33
+ map_type :boolean, :to => SimpleForm::Inputs::BooleanInput
34
+
35
+ def self.discovery_cache
36
+ @discovery_cache ||= {}
37
+ end
38
+
39
+ def initialize(*) #:nodoc:
40
+ super
41
+ @defaults = options[:defaults]
42
+ @wrapper = SimpleForm.wrapper(options[:wrapper] || SimpleForm.default_wrapper)
43
+ end
44
+
45
+ # Basic input helper, combines all components in the stack to generate
46
+ # input html based on options the user define and some guesses through
47
+ # database column information. By default a call to input will generate
48
+ # label + input + hint (when defined) + errors (when exists), and all can
49
+ # be configured inside a wrapper html.
50
+ #
51
+ # == Examples
52
+ #
53
+ # # Imagine @user has error "can't be blank" on name
54
+ # simple_form_for @user do |f|
55
+ # f.input :name, :hint => 'My hint'
56
+ # end
57
+ #
58
+ # This is the output html (only the input portion, not the form):
59
+ #
60
+ # <label class="string required" for="user_name">
61
+ # <abbr title="required">*</abbr> Super User Name!
62
+ # </label>
63
+ # <input class="string required" id="user_name" maxlength="100"
64
+ # name="user[name]" size="100" type="text" value="Carlos" />
65
+ # <span class="hint">My hint</span>
66
+ # <span class="error">can't be blank</span>
67
+ #
68
+ # Each database type will render a default input, based on some mappings and
69
+ # heuristic to determine which is the best option.
70
+ #
71
+ # You have some options for the input to enable/disable some functions:
72
+ #
73
+ # :as => allows you to define the input type you want, for instance you
74
+ # can use it to generate a text field for a date column.
75
+ #
76
+ # :required => defines whether this attribute is required or not. True
77
+ # by default.
78
+ #
79
+ # The fact SimpleForm is built in components allow the interface to be unified.
80
+ # So, for instance, if you need to disable :hint for a given input, you can pass
81
+ # :hint => false. The same works for :error, :label and :wrapper.
82
+ #
83
+ # Besides the html for any component can be changed. So, if you want to change
84
+ # the label html you just need to give a hash to :label_html. To configure the
85
+ # input html, supply :input_html instead and so on.
86
+ #
87
+ # == Options
88
+ #
89
+ # Some inputs, as datetime, time and select allow you to give extra options, like
90
+ # prompt and/or include blank. Such options are given in plainly:
91
+ #
92
+ # f.input :created_at, :include_blank => true
93
+ #
94
+ # == Collection
95
+ #
96
+ # When playing with collections (:radio_buttons, :check_boxes and :select
97
+ # inputs), you have three extra options:
98
+ #
99
+ # :collection => use to determine the collection to generate the radio or select
100
+ #
101
+ # :label_method => the method to apply on the array collection to get the label
102
+ #
103
+ # :value_method => the method to apply on the array collection to get the value
104
+ #
105
+ # == Priority
106
+ #
107
+ # Some inputs, as :time_zone and :country accepts a :priority option. If none is
108
+ # given SimpleForm.time_zone_priority and SimpleForm.country_priority are used respectively.
109
+ #
110
+ def input(attribute_name, options={}, &block)
111
+ options = @defaults.deep_dup.deep_merge(options) if @defaults
112
+ input = find_input(attribute_name, options, &block)
113
+
114
+ chosen =
115
+ if name = options[:wrapper] || find_wrapper_mapping(input.input_type)
116
+ name.respond_to?(:render) ? name : SimpleForm.wrapper(name)
117
+ else
118
+ wrapper
119
+ end
120
+
121
+ chosen.render input
122
+ end
123
+ alias :attribute :input
124
+
125
+ # Creates a input tag for the given attribute. All the given options
126
+ # are sent as :input_html.
127
+ #
128
+ # == Examples
129
+ #
130
+ # simple_form_for @user do |f|
131
+ # f.input_field :name
132
+ # end
133
+ #
134
+ # This is the output html (only the input portion, not the form):
135
+ #
136
+ # <input class="string required" id="user_name" maxlength="100"
137
+ # name="user[name]" size="100" type="text" value="Carlos" />
138
+ #
139
+ def input_field(attribute_name, options={})
140
+ options = options.dup
141
+ options[:input_html] = options.except(:as, :collection, :label_method, :value_method)
142
+ options = @defaults.deep_dup.deep_merge(options) if @defaults
143
+
144
+ SimpleForm::Wrappers::Root.new([:input], :wrapper => false).render find_input(attribute_name, options)
145
+ end
146
+
147
+ # Helper for dealing with association selects/radios, generating the
148
+ # collection automatically. It's just a wrapper to input, so all options
149
+ # supported in input are also supported by association. Some extra options
150
+ # can also be given:
151
+ #
152
+ # == Examples
153
+ #
154
+ # simple_form_for @user do |f|
155
+ # f.association :company # Company.all
156
+ # end
157
+ #
158
+ # f.association :company, :collection => Company.all(:order => 'name')
159
+ # # Same as using :order option, but overriding collection
160
+ #
161
+ # == Block
162
+ #
163
+ # When a block is given, association simple behaves as a proxy to
164
+ # simple_fields_for:
165
+ #
166
+ # f.association :company do |c|
167
+ # c.input :name
168
+ # c.input :type
169
+ # end
170
+ #
171
+ # From the options above, only :collection can also be supplied.
172
+ #
173
+ def association(association, options={}, &block)
174
+ options = options.dup
175
+
176
+ return simple_fields_for(*[association,
177
+ options.delete(:collection), options].compact, &block) if block_given?
178
+
179
+ raise ArgumentError, "Association cannot be used in forms not associated with an object" unless @object
180
+
181
+ reflection = find_association_reflection(association)
182
+ raise "Association #{association.inspect} not found" unless reflection
183
+
184
+ options[:as] ||= :select
185
+ options[:collection] ||= options.fetch(:collection) {
186
+ reflection.klass.all(reflection.options.slice(:conditions, :order))
187
+ }
188
+
189
+ attribute = case reflection.macro
190
+ when :belongs_to
191
+ (reflection.respond_to?(:options) && reflection.options[:foreign_key]) || :"#{reflection.name}_id"
192
+ when :has_one
193
+ raise ArgumentError, ":has_one associations are not supported by f.association"
194
+ else
195
+ if options[:as] == :select
196
+ html_options = options[:input_html] ||= {}
197
+ html_options[:size] ||= 5
198
+ html_options[:multiple] = true unless html_options.key?(:multiple)
199
+ end
200
+
201
+ # Force the association to be preloaded for performance.
202
+ if options[:preload] != false && object.respond_to?(association)
203
+ target = object.send(association)
204
+ target.to_a if target.respond_to?(:to_a)
205
+ end
206
+
207
+ :"#{reflection.name.to_s.singularize}_ids"
208
+ end
209
+
210
+ input(attribute, options.merge(:reflection => reflection))
211
+ end
212
+
213
+ # Creates a button:
214
+ #
215
+ # form_for @user do |f|
216
+ # f.button :submit
217
+ # end
218
+ #
219
+ # It just acts as a proxy to method name given. We also alias original Rails
220
+ # button implementation (3.2 forward (to delegate to the original when
221
+ # calling `f.button :button`.
222
+ #
223
+ # TODO: remove if condition when supporting only Rails 3.2 forward.
224
+ alias_method :button_button, :button if method_defined?(:button)
225
+ def button(type, *args, &block)
226
+ options = args.extract_options!.dup
227
+ options[:class] = [SimpleForm.button_class, options[:class]].compact
228
+ args << options
229
+ if respond_to?("#{type}_button")
230
+ send("#{type}_button", *args, &block)
231
+ else
232
+ send(type, *args, &block)
233
+ end
234
+ end
235
+
236
+ # Creates an error tag based on the given attribute, only when the attribute
237
+ # contains errors. All the given options are sent as :error_html.
238
+ #
239
+ # == Examples
240
+ #
241
+ # f.error :name
242
+ # f.error :name, :id => "cool_error"
243
+ #
244
+ def error(attribute_name, options={})
245
+ options = options.dup
246
+
247
+ options[:error_html] = options.except(:error_tag, :error_prefix, :error_method)
248
+ column = find_attribute_column(attribute_name)
249
+ input_type = default_input_type(attribute_name, column, options)
250
+ wrapper.find(:error).
251
+ render(SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options))
252
+ end
253
+
254
+ # Return the error but also considering its name. This is used
255
+ # when errors for a hidden field need to be shown.
256
+ #
257
+ # == Examples
258
+ #
259
+ # f.full_error :token #=> <span class="error">Token is invalid</span>
260
+ #
261
+ def full_error(attribute_name, options={})
262
+ options = options.dup
263
+
264
+ options[:error_prefix] ||= if object.class.respond_to?(:human_attribute_name)
265
+ object.class.human_attribute_name(attribute_name.to_s)
266
+ else
267
+ attribute_name.to_s.humanize
268
+ end
269
+
270
+ error(attribute_name, options)
271
+ end
272
+
273
+ # Creates a hint tag for the given attribute. Accepts a symbol indicating
274
+ # an attribute for I18n lookup or a string. All the given options are sent
275
+ # as :hint_html.
276
+ #
277
+ # == Examples
278
+ #
279
+ # f.hint :name # Do I18n lookup
280
+ # f.hint :name, :id => "cool_hint"
281
+ # f.hint "Don't forget to accept this"
282
+ #
283
+ def hint(attribute_name, options={})
284
+ options = options.dup
285
+
286
+ options[:hint_html] = options.except(:hint_tag, :hint)
287
+ if attribute_name.is_a?(String)
288
+ options[:hint] = attribute_name
289
+ attribute_name, column, input_type = nil, nil, nil
290
+ else
291
+ column = find_attribute_column(attribute_name)
292
+ input_type = default_input_type(attribute_name, column, options)
293
+ end
294
+
295
+ wrapper.find(:hint).
296
+ render(SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options))
297
+ end
298
+
299
+ # Creates a default label tag for the given attribute. You can give a label
300
+ # through the :label option or using i18n. All the given options are sent
301
+ # as :label_html.
302
+ #
303
+ # == Examples
304
+ #
305
+ # f.label :name # Do I18n lookup
306
+ # f.label :name, "Name" # Same behavior as Rails, do not add required tag
307
+ # f.label :name, :label => "Name" # Same as above, but adds required tag
308
+ #
309
+ # f.label :name, :required => false
310
+ # f.label :name, :id => "cool_label"
311
+ #
312
+ def label(attribute_name, *args)
313
+ return super if args.first.is_a?(String) || block_given?
314
+
315
+ options = args.extract_options!.dup
316
+ options[:label_html] = options.except(:label, :required, :as)
317
+
318
+ column = find_attribute_column(attribute_name)
319
+ input_type = default_input_type(attribute_name, column, options)
320
+ SimpleForm::Inputs::Base.new(self, attribute_name, column, input_type, options).label
321
+ end
322
+
323
+ # Creates an error notification message that only appears when the form object
324
+ # has some error. You can give a specific message with the :message option,
325
+ # otherwise it will look for a message using I18n. All other options given are
326
+ # passed straight as html options to the html tag.
327
+ #
328
+ # == Examples
329
+ #
330
+ # f.error_notification
331
+ # f.error_notification :message => 'Something went wrong'
332
+ # f.error_notification :id => 'user_error_message', :class => 'form_error'
333
+ #
334
+ def error_notification(options={})
335
+ SimpleForm::ErrorNotification.new(self, options).render
336
+ end
337
+
338
+ # Extract the model names from the object_name mess, ignoring numeric and
339
+ # explicit child indexes.
340
+ #
341
+ # Example:
342
+ #
343
+ # route[blocks_attributes][0][blocks_learning_object_attributes][1][foo_attributes]
344
+ # ["route", "blocks", "blocks_learning_object", "foo"]
345
+ #
346
+ def lookup_model_names
347
+ @lookup_model_names ||= begin
348
+ child_index = options[:child_index]
349
+ names = object_name.to_s.scan(/([a-zA-Z_]+)/).flatten
350
+ names.delete(child_index) if child_index
351
+ names.each { |name| name.gsub!('_attributes', '') }
352
+ names.freeze
353
+ end
354
+ end
355
+
356
+ # The action to be used in lookup.
357
+ def lookup_action
358
+ @lookup_action ||= begin
359
+ action = template.controller.action_name
360
+ return unless action
361
+ action = action.to_sym
362
+ ACTIONS[action] || action
363
+ end
364
+ end
365
+
366
+ private
367
+
368
+ # Find an input based on the attribute name.
369
+ def find_input(attribute_name, options={}, &block) #:nodoc:
370
+ column = find_attribute_column(attribute_name)
371
+ input_type = default_input_type(attribute_name, column, options)
372
+
373
+ if input_type == :radio
374
+ SimpleForm.deprecation_warn "Using `:as => :radio` as input type is " \
375
+ "deprecated, please change it to `:as => :radio_buttons`."
376
+ input_type = :radio_buttons
377
+ end
378
+
379
+ if block_given?
380
+ SimpleForm::Inputs::BlockInput.new(self, attribute_name, column, input_type, options, &block)
381
+ else
382
+ find_mapping(input_type).new(self, attribute_name, column, input_type, options)
383
+ end
384
+ end
385
+
386
+ # Attempt to guess the better input type given the defined options. By
387
+ # default alwayls fallback to the user :as option, or to a :select when a
388
+ # collection is given.
389
+ def default_input_type(attribute_name, column, options) #:nodoc:
390
+ return options[:as].to_sym if options[:as]
391
+ return :select if options[:collection]
392
+ custom_type = find_custom_type(attribute_name.to_s) and return custom_type
393
+
394
+ input_type = column.try(:type)
395
+ case input_type
396
+ when :timestamp
397
+ :datetime
398
+ when :string, nil
399
+ case attribute_name.to_s
400
+ when /password/ then :password
401
+ when /time_zone/ then :time_zone
402
+ when /country/ then :country
403
+ when /email/ then :email
404
+ when /phone/ then :tel
405
+ when /url/ then :url
406
+ else
407
+ file_method?(attribute_name) ? :file : (input_type || :string)
408
+ end
409
+ else
410
+ input_type
411
+ end
412
+ end
413
+
414
+ def find_custom_type(attribute_name) #:nodoc:
415
+ SimpleForm.input_mappings.find { |match, type|
416
+ attribute_name =~ match
417
+ }.try(:last) if SimpleForm.input_mappings
418
+ end
419
+
420
+ def file_method?(attribute_name) #:nodoc:
421
+ file = @object.send(attribute_name) if @object.respond_to?(attribute_name)
422
+ file && SimpleForm.file_methods.any? { |m| file.respond_to?(m) }
423
+ end
424
+
425
+ def find_attribute_column(attribute_name) #:nodoc:
426
+ if @object.respond_to?(:column_for_attribute)
427
+ @object.column_for_attribute(attribute_name)
428
+ end
429
+ end
430
+
431
+ def find_association_reflection(association) #:nodoc:
432
+ if @object.class.respond_to?(:reflect_on_association)
433
+ @object.class.reflect_on_association(association)
434
+ end
435
+ end
436
+
437
+ # Attempts to find a mapping. It follows the following rules:
438
+ #
439
+ # 1) It tries to find a registered mapping, if succeeds:
440
+ # a) Try to find an alternative with the same name in the Object scope
441
+ # b) Or use the found mapping
442
+ # 2) If not, fallbacks to #{input_type}Input
443
+ # 3) If not, fallbacks to SimpleForm::Inputs::#{input_type}Input
444
+ def find_mapping(input_type) #:nodoc:
445
+ discovery_cache[input_type] ||=
446
+ if mapping = self.class.mappings[input_type]
447
+ mapping_override(mapping) || mapping
448
+ else
449
+ camelized = "#{input_type.to_s.camelize}Input"
450
+ attempt_mapping(camelized, Object) || attempt_mapping(camelized, self.class) ||
451
+ raise("No input found for #{input_type}")
452
+ end
453
+ end
454
+
455
+ def find_wrapper_mapping(input_type) #:nodoc:
456
+ SimpleForm.wrapper_mappings && SimpleForm.wrapper_mappings[input_type]
457
+ end
458
+
459
+ # If cache_discovery is enabled, use the class level cache that persists
460
+ # between requests, otherwise use the instance one.
461
+ def discovery_cache #:nodoc:
462
+ if SimpleForm.cache_discovery
463
+ self.class.discovery_cache
464
+ else
465
+ @discovery_cache ||= {}
466
+ end
467
+ end
468
+
469
+ def mapping_override(klass) #:nodoc:
470
+ name = klass.name
471
+ if name =~ /^SimpleForm::Inputs/
472
+ attempt_mapping name.split("::").last, Object
473
+ end
474
+ end
475
+
476
+ def attempt_mapping(mapping, at) #:nodoc:
477
+ return if SimpleForm.inputs_discovery == false && at == Object
478
+
479
+ begin
480
+ at.const_get(mapping)
481
+ rescue NameError => e
482
+ raise if e.message !~ /#{mapping}$/
483
+ end
484
+ end
485
+ end
486
+ end
@@ -36,8 +36,8 @@ module SimpleForm
36
36
  end
37
37
  end
38
38
 
39
- def find_validator(validator)
40
- attribute_validators.find { |v| validator === v } if has_validators?
39
+ def find_validator(kind)
40
+ attribute_validators.find { |v| v.kind == kind } if has_validators?
41
41
  end
42
42
  end
43
43
  end
@@ -1,3 +1,5 @@
1
+ require 'simple_form/i18n_cache'
2
+
1
3
  module SimpleForm
2
4
  module Inputs
3
5
  class Base
@@ -48,6 +50,7 @@ module SimpleForm
48
50
  def initialize(builder, attribute_name, column, input_type, options = {})
49
51
  super
50
52
 
53
+ options = options.dup
51
54
  @builder = builder
52
55
  @attribute_name = attribute_name
53
56
  @column = column
@@ -59,9 +62,7 @@ module SimpleForm
59
62
  # Notice that html_options_for receives a reference to input_html_classes.
60
63
  # This means that classes added dynamically to input_html_classes will
61
64
  # still propagate to input_html_options.
62
- @html_classes = SimpleForm.additional_classes_for(:input) {
63
- [input_type, required_class, readonly_class, disabled_class].compact
64
- }
65
+ @html_classes = SimpleForm.additional_classes_for(:input) { additional_classes }
65
66
 
66
67
  @input_html_classes = @html_classes.dup
67
68
  @input_html_options = html_options_for(:input, input_html_classes).tap do |o|
@@ -79,6 +80,14 @@ module SimpleForm
79
80
  options
80
81
  end
81
82
 
83
+ def additional_classes
84
+ @additional_classes ||= [input_type, required_class, readonly_class, disabled_class].compact
85
+ end
86
+
87
+ def input_class
88
+ "#{lookup_model_names.join("_")}_#{reflection_or_attribute_name}"
89
+ end
90
+
82
91
  private
83
92
 
84
93
  def add_size!
@@ -86,7 +95,22 @@ module SimpleForm
86
95
  end
87
96
 
88
97
  def limit
89
- column && column.limit
98
+ if column
99
+ decimal_or_float? ? decimal_limit : column_limit
100
+ end
101
+ end
102
+
103
+ def column_limit
104
+ column.limit
105
+ end
106
+
107
+ # Add one for decimal point
108
+ def decimal_limit
109
+ column_limit && (column_limit + 1)
110
+ end
111
+
112
+ def decimal_or_float?
113
+ column.number? && column.type != :integer
90
114
  end
91
115
 
92
116
  def nested_boolean_style?
@@ -95,14 +119,15 @@ module SimpleForm
95
119
 
96
120
  # Find reflection name when available, otherwise use attribute
97
121
  def reflection_or_attribute_name
98
- reflection ? reflection.name : attribute_name
122
+ @reflection_or_attribute_name ||= reflection ? reflection.name : attribute_name
99
123
  end
100
124
 
101
125
  # Retrieve options for the given namespace from the options hash
102
126
  def html_options_for(namespace, css_classes)
103
- html_options = options[:"#{namespace}_html"] || {}
127
+ html_options = options[:"#{namespace}_html"]
128
+ html_options = html_options ? html_options.dup : {}
104
129
  css_classes << html_options[:class] if html_options.key?(:class)
105
- html_options[:class] = css_classes
130
+ html_options[:class] = css_classes unless css_classes.empty?
106
131
  html_options
107
132
  end
108
133
 
@@ -151,7 +176,7 @@ module SimpleForm
151
176
  lookups << :"#{joined_model_names}.#{reflection_or_attribute_name}"
152
177
  end
153
178
  lookups << :"defaults.#{lookup_action}.#{reflection_or_attribute_name}"
154
- lookups << :"defaults.#{attribute_name}"
179
+ lookups << :"defaults.#{reflection_or_attribute_name}"
155
180
  lookups << default
156
181
 
157
182
  I18n.t(lookups.shift, :scope => :"simple_form.#{namespace}", :default => lookups).presence