simple_form 2.1.3 → 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


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

Files changed (72) hide show
  1. data/CHANGELOG.md +6 -54
  2. data/README.md +129 -111
  3. data/lib/generators/simple_form/install_generator.rb +4 -4
  4. data/lib/generators/simple_form/templates/README +2 -2
  5. data/lib/generators/simple_form/templates/config/initializers/simple_form.rb +8 -11
  6. data/lib/generators/simple_form/templates/config/initializers/simple_form_bootstrap.rb +16 -16
  7. data/lib/generators/simple_form/templates/config/initializers/simple_form_foundation.rb +3 -3
  8. data/lib/simple_form.rb +31 -47
  9. data/lib/simple_form/action_view_extensions/builder.rb +0 -319
  10. data/lib/simple_form/action_view_extensions/builder.rb.orig +247 -0
  11. data/lib/simple_form/action_view_extensions/form_helper.rb +1 -1
  12. data/lib/simple_form/components.rb +1 -1
  13. data/lib/simple_form/components/errors.rb +1 -7
  14. data/lib/simple_form/components/hints.rb +2 -7
  15. data/lib/simple_form/components/html5.rb +1 -1
  16. data/lib/simple_form/components/labels.rb +4 -4
  17. data/lib/simple_form/components/maxlength.rb +1 -8
  18. data/lib/simple_form/error_notification.rb +2 -2
  19. data/lib/simple_form/form_builder.rb +144 -46
  20. data/lib/simple_form/form_builder.rb.orig +486 -0
  21. data/lib/simple_form/helpers.rb +1 -1
  22. data/lib/simple_form/inputs/base.rb +3 -10
  23. data/lib/simple_form/inputs/block_input.rb +1 -1
  24. data/lib/simple_form/inputs/boolean_input.rb +6 -6
  25. data/lib/simple_form/inputs/collection_input.rb +7 -7
  26. data/lib/simple_form/inputs/numeric_input.rb +0 -6
  27. data/lib/simple_form/inputs/password_input.rb +0 -1
  28. data/lib/simple_form/inputs/string_input.rb +0 -1
  29. data/lib/simple_form/railtie.rb +7 -0
  30. data/lib/simple_form/tags.rb +61 -0
  31. data/lib/simple_form/version.rb +1 -1
  32. data/lib/simple_form/version.rb.orig +7 -0
  33. data/lib/simple_form/wrappers.rb +1 -1
  34. data/lib/simple_form/wrappers/builder.rb +5 -29
  35. data/lib/simple_form/wrappers/many.rb +1 -1
  36. data/lib/simple_form/wrappers/root.rb +1 -1
  37. data/test/action_view_extensions/builder_test.rb +67 -87
  38. data/test/action_view_extensions/form_helper_test.rb +16 -16
  39. data/test/components/label_test.rb +46 -46
  40. data/test/form_builder/association_test.rb +23 -23
  41. data/test/form_builder/button_test.rb +4 -4
  42. data/test/form_builder/error_notification_test.rb +8 -8
  43. data/test/form_builder/error_test.rb +18 -65
  44. data/test/form_builder/general_test.rb +45 -65
  45. data/test/form_builder/hint_test.rb +23 -29
  46. data/test/form_builder/input_field_test.rb +12 -12
  47. data/test/form_builder/label_test.rb +6 -16
  48. data/test/form_builder/wrapper_test.rb +21 -21
  49. data/test/inputs/boolean_input_test.rb +23 -35
  50. data/test/inputs/collection_check_boxes_input_test.rb +55 -55
  51. data/test/inputs/collection_radio_buttons_input_test.rb +70 -79
  52. data/test/inputs/collection_select_input_test.rb +45 -51
  53. data/test/inputs/datetime_input_test.rb +11 -11
  54. data/test/inputs/disabled_test.rb +10 -10
  55. data/test/inputs/discovery_test.rb +4 -4
  56. data/test/inputs/file_input_test.rb +1 -1
  57. data/test/inputs/general_test.rb +12 -12
  58. data/test/inputs/grouped_collection_select_input_test.rb +20 -20
  59. data/test/inputs/hidden_input_test.rb +1 -1
  60. data/test/inputs/numeric_input_test.rb +3 -3
  61. data/test/inputs/priority_input_test.rb +3 -3
  62. data/test/inputs/readonly_test.rb +12 -12
  63. data/test/inputs/required_test.rb +5 -5
  64. data/test/inputs/string_input_test.rb +10 -25
  65. data/test/inputs/text_input_test.rb +1 -1
  66. data/test/support/misc_helpers.rb +24 -24
  67. data/test/support/mock_controller.rb +6 -6
  68. data/test/support/models.rb +37 -46
  69. data/test/test_helper.rb +20 -20
  70. metadata +49 -24
  71. checksums.yaml +0 -7
  72. data/lib/simple_form/core_ext/hash.rb +0 -16
@@ -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