simple_form 2.0.4 → 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.

Potentially problematic release.


This version of simple_form might be problematic. Click here for more details.

Files changed (36) hide show
  1. data/CHANGELOG.md +33 -301
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +30 -12
  4. data/lib/generators/simple_form/install_generator.rb +7 -4
  5. data/lib/generators/simple_form/templates/config/initializers/simple_form_foundation.rb +26 -0
  6. data/lib/simple_form.rb +11 -8
  7. data/lib/simple_form/action_view_extensions/builder.rb +32 -18
  8. data/lib/simple_form/action_view_extensions/builder.rb.orig +247 -0
  9. data/lib/simple_form/components.rb +12 -10
  10. data/lib/simple_form/components/hints.rb +1 -1
  11. data/lib/simple_form/components/label_input.rb +1 -1
  12. data/lib/simple_form/components/maxlength.rb +1 -1
  13. data/lib/simple_form/components/min_max.rb +1 -1
  14. data/lib/simple_form/components/pattern.rb +1 -1
  15. data/lib/simple_form/form_builder.rb +6 -3
  16. data/lib/simple_form/form_builder.rb.orig +486 -0
  17. data/lib/simple_form/helpers/validators.rb +2 -2
  18. data/lib/simple_form/inputs.rb +19 -17
  19. data/lib/simple_form/inputs/base.rb +8 -2
  20. data/lib/simple_form/inputs/date_time_input.rb +0 -4
  21. data/lib/simple_form/version.rb +1 -1
  22. data/lib/simple_form/version.rb.orig +7 -0
  23. data/lib/simple_form/wrappers/root.rb +3 -1
  24. data/test/action_view_extensions/builder_test.rb +113 -67
  25. data/test/action_view_extensions/form_helper_test.rb +5 -0
  26. data/test/components/label_test.rb +27 -17
  27. data/test/form_builder/association_test.rb +10 -7
  28. data/test/form_builder/general_test.rb +48 -25
  29. data/test/form_builder/input_field_test.rb +45 -0
  30. data/test/form_builder/wrapper_test.rb +22 -0
  31. data/test/generators/simple_form_generator_test.rb +8 -0
  32. data/test/inputs/datetime_input_test.rb +2 -2
  33. data/test/inputs/grouped_collection_select_input_test.rb +15 -0
  34. data/test/support/misc_helpers.rb +6 -0
  35. data/test/test_helper.rb +6 -1
  36. metadata +12 -2
@@ -33,7 +33,7 @@ module SimpleForm
33
33
  end
34
34
 
35
35
  def find_numericality_validator
36
- find_validator(ActiveModel::Validations::NumericalityValidator)
36
+ find_validator(:numericality)
37
37
  end
38
38
 
39
39
  def evaluate_numericality_validator_option(option)
@@ -19,7 +19,7 @@ module SimpleForm
19
19
  end
20
20
 
21
21
  def find_pattern_validator
22
- find_validator(ActiveModel::Validations::FormatValidator)
22
+ find_validator(:format)
23
23
  end
24
24
 
25
25
  def evaluate_format_validator_option(option)
@@ -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
@@ -134,7 +135,9 @@ module SimpleForm
134
135
  def input_field(attribute_name, options={})
135
136
  options = options.dup
136
137
  options[:input_html] = options.except(:as, :collection, :label_method, :value_method)
137
- 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)
138
141
  end
139
142
 
140
143
  # Helper for dealing with association selects/radios, generating the
@@ -445,7 +448,7 @@ module SimpleForm
445
448
  end
446
449
  end
447
450
 
448
- def find_wrapper_mapping(input_type)
451
+ def find_wrapper_mapping(input_type) #:nodoc:
449
452
  SimpleForm.wrapper_mappings && SimpleForm.wrapper_mappings[input_type]
450
453
  end
451
454
 
@@ -472,7 +475,7 @@ module SimpleForm
472
475
  begin
473
476
  at.const_get(mapping)
474
477
  rescue NameError => e
475
- e.message =~ /#{mapping}$/ ? nil : raise
478
+ raise if e.message !~ /#{mapping}$/
476
479
  end
477
480
  end
478
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