justinfrench-formtastic 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +58 -41
- data/lib/formtastic.rb +319 -257
- data/lib/locale/en.yml +1 -6
- data/spec/formtastic_spec.rb +984 -1166
- metadata +2 -2
data/lib/formtastic.rb
CHANGED
@@ -14,14 +14,28 @@ module Formtastic #:nodoc:
|
|
14
14
|
@@required_string = proc { %{<abbr title="#{I18n.t 'formtastic.required', :default => 'required'}">*</abbr>} }
|
15
15
|
@@optional_string = ''
|
16
16
|
@@inline_errors = :sentence
|
17
|
-
@@label_str_method = :
|
17
|
+
@@label_str_method = :humanize
|
18
18
|
@@collection_label_methods = %w[to_label display_name full_name name title username login value to_s]
|
19
19
|
@@inline_order = [ :input, :hints, :errors ]
|
20
|
+
@@file_methods = [ :file?, :public_filename ]
|
20
21
|
|
21
|
-
cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string,
|
22
|
+
cattr_accessor :default_text_field_size, :all_fields_required_by_default, :required_string,
|
23
|
+
:optional_string, :inline_errors, :label_str_method, :collection_label_methods,
|
24
|
+
:inline_order, :file_methods
|
22
25
|
|
23
|
-
|
26
|
+
# Keeps simple mappings in a hash
|
27
|
+
#
|
28
|
+
INPUT_MAPPINGS = {
|
29
|
+
:string => :text_field,
|
30
|
+
:password => :password_field,
|
31
|
+
:numeric => :text_field,
|
32
|
+
:text => :text_area,
|
33
|
+
:file => :file_field
|
34
|
+
}
|
35
|
+
STRING_MAPPINGS = [ :string, :password, :numeric ]
|
24
36
|
|
37
|
+
attr_accessor :template
|
38
|
+
attr_writer :nested_child_index
|
25
39
|
|
26
40
|
# Returns a suitable form input for the given +method+, using the database column information
|
27
41
|
# and other factors (like the method name) to figure out what you probably want.
|
@@ -32,6 +46,7 @@ module Formtastic #:nodoc:
|
|
32
46
|
# * :label - use something other than the method name as the label (or fieldset legend) text
|
33
47
|
# * :required - specify if the column is required (true) or not (false)
|
34
48
|
# * :hint - provide some text to hint or help the user provide the correct information for a field
|
49
|
+
# * :input_html - provide options that will be passed down to the generated input
|
35
50
|
#
|
36
51
|
# Input Types:
|
37
52
|
#
|
@@ -39,15 +54,14 @@ module Formtastic #:nodoc:
|
|
39
54
|
# but there are a few special cases and some simplification (:integer, :float and :decimal
|
40
55
|
# columns all map to a single numeric_input, for example).
|
41
56
|
#
|
42
|
-
# * :select (a select menu for
|
43
|
-
# * :radio (a set of radio inputs for
|
57
|
+
# * :select (a select menu for associations) - default to association names
|
58
|
+
# * :radio (a set of radio inputs for associations) - default to association names
|
44
59
|
# * :password (a password input) - default for :string column types with 'password' in the method name
|
45
60
|
# * :text (a textarea) - default for :text column types
|
46
61
|
# * :date (a date select) - default for :date column types
|
47
62
|
# * :datetime (a date and time select) - default for :datetime and :timestamp column types
|
48
63
|
# * :time (a time select) - default for :time column types
|
49
|
-
# * :boolean (a checkbox) - default for :boolean column types
|
50
|
-
# * :boolean_select (a yes/no select box)
|
64
|
+
# * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio)
|
51
65
|
# * :string (a text field) - default for :string column types
|
52
66
|
# * :numeric (a text field, like string) - default for :integer, :float and :decimal column types
|
53
67
|
#
|
@@ -61,29 +75,28 @@ module Formtastic #:nodoc:
|
|
61
75
|
# <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %>
|
62
76
|
# <% end %>
|
63
77
|
# <% end %>
|
78
|
+
#
|
64
79
|
def input(method, options = {})
|
65
|
-
|
80
|
+
options[:required] = method_required?(method, options[:required])
|
81
|
+
options[:as] ||= default_input_type(method)
|
66
82
|
|
83
|
+
options[:label] ||= if @object
|
84
|
+
@object.class.human_attribute_name(method.to_s)
|
85
|
+
else
|
86
|
+
method.to_s.send(@@label_str_method)
|
87
|
+
end
|
67
88
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
input_method = "#{options[:as]}_input"
|
89
|
+
if [:boolean_select, :boolean_radio].include?(options[:as])
|
90
|
+
::ActiveSupport::Deprecation.warn(":as => :#{options[:as]} is deprecated, use :as => :#{options[:as].to_s[8..-1]} instead", caller[3..-1])
|
91
|
+
end
|
72
92
|
|
73
|
-
html_class = [
|
74
|
-
|
75
|
-
(options[:required] ? 'required' : 'optional'),
|
76
|
-
(@object.errors.on(method.to_s) ? 'error' : nil)
|
77
|
-
].compact.join(" ")
|
93
|
+
html_class = [ options[:as], (options[:required] ? :required : :optional) ].join(' ')
|
94
|
+
html_class << ' error' if @object && @object.errors.on(method.to_s)
|
78
95
|
|
79
96
|
html_id = generate_html_id(method)
|
80
97
|
|
81
98
|
list_item_content = @@inline_order.map do |type|
|
82
|
-
|
83
|
-
send(input_method, method, options)
|
84
|
-
else
|
85
|
-
send(:"inline_#{type}", method, options)
|
86
|
-
end
|
99
|
+
send(:"inline_#{type}_for", method, options)
|
87
100
|
end.compact.join("\n")
|
88
101
|
|
89
102
|
return template.content_tag(:li, list_item_content, { :id => html_id, :class => html_class })
|
@@ -221,20 +234,28 @@ module Formtastic #:nodoc:
|
|
221
234
|
html_options[:class] ||= "inputs"
|
222
235
|
|
223
236
|
if fields_for_object = html_options.delete(:for)
|
224
|
-
|
237
|
+
html_options.merge!(:parent_builder => self)
|
238
|
+
inputs_for_nested_attributes(fields_for_object, args << html_options,
|
239
|
+
html_options.delete(:for_options) || {}, &block)
|
225
240
|
elsif block_given?
|
226
241
|
field_set_and_list_wrapping(html_options, &block)
|
227
242
|
else
|
228
|
-
if args.empty?
|
229
|
-
|
243
|
+
if @object && args.empty?
|
244
|
+
# Get all belongs_to association
|
245
|
+
args = @object.class.reflections.map { |n,_| n if _.macro == :belongs_to }
|
246
|
+
|
247
|
+
# Get content columns and remove timestamps columns from it
|
230
248
|
args += @object.class.content_columns.map(&:name)
|
249
|
+
args -= %w[created_at updated_at created_on updated_on]
|
250
|
+
|
251
|
+
args.compact!
|
231
252
|
end
|
232
253
|
contents = args.map { |method| input(method.to_sym) }
|
233
254
|
|
234
255
|
field_set_and_list_wrapping(html_options, contents)
|
235
256
|
end
|
236
257
|
end
|
237
|
-
|
258
|
+
alias :input_field_set :inputs
|
238
259
|
|
239
260
|
# Creates a fieldset and ol tag wrapping for form buttons / actions as list items.
|
240
261
|
# See inputs documentation for a full example. The fieldset's default class attriute
|
@@ -253,7 +274,7 @@ module Formtastic #:nodoc:
|
|
253
274
|
field_set_and_list_wrapping(html_options, contents)
|
254
275
|
end
|
255
276
|
end
|
256
|
-
|
277
|
+
alias :button_field_set :buttons
|
257
278
|
|
258
279
|
# Creates a submit input tag with the value "Save [model name]" (for existing records) or
|
259
280
|
# "Create [model name]" (for new records) by default:
|
@@ -263,11 +284,12 @@ module Formtastic #:nodoc:
|
|
263
284
|
# The value of the button text can be overridden:
|
264
285
|
#
|
265
286
|
# <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" />
|
266
|
-
|
287
|
+
#
|
288
|
+
def commit_button(value=nil, options = {})
|
289
|
+
value ||= save_or_create_button_text
|
267
290
|
template.content_tag(:li, template.submit_tag(value), :class => "commit")
|
268
291
|
end
|
269
292
|
|
270
|
-
|
271
293
|
# A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
|
272
294
|
# for nesting forms:
|
273
295
|
#
|
@@ -286,6 +308,7 @@ module Formtastic #:nodoc:
|
|
286
308
|
# </ol>
|
287
309
|
# </fieldset>
|
288
310
|
# </form>
|
311
|
+
#
|
289
312
|
def semantic_fields_for(record_or_name_or_array, *args, &block)
|
290
313
|
opts = args.extract_options!
|
291
314
|
opts.merge!(:builder => Formtastic::SemanticFormBuilder)
|
@@ -295,54 +318,68 @@ module Formtastic #:nodoc:
|
|
295
318
|
|
296
319
|
protected
|
297
320
|
|
298
|
-
# Deals with :for option when it's supplied to inputs methods.
|
321
|
+
# Deals with :for option when it's supplied to inputs methods. Additional
|
322
|
+
# options to be passed down to :for should be supplied using :for_options
|
323
|
+
# key.
|
324
|
+
#
|
299
325
|
# It should raise an error if a block with arity zero is given.
|
300
326
|
#
|
301
|
-
def inputs_for_nested_attributes(fields_for_object,
|
327
|
+
def inputs_for_nested_attributes(fields_for_object, inputs, options, &block)
|
302
328
|
fields_for_block = if block_given?
|
303
329
|
raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
|
304
330
|
'but the block does not accept any argument.' if block.arity <= 0
|
305
331
|
|
306
|
-
proc { |f| f.inputs(*
|
332
|
+
proc { |f| f.inputs(*inputs){ block.call(f) } }
|
307
333
|
else
|
308
|
-
proc { |f| f.inputs(*
|
334
|
+
proc { |f| f.inputs(*inputs) }
|
309
335
|
end
|
310
336
|
|
311
|
-
semantic_fields_for(*Array(fields_for_object), &fields_for_block)
|
337
|
+
semantic_fields_for(*(Array(fields_for_object) << options), &fields_for_block)
|
312
338
|
end
|
313
339
|
|
314
|
-
#
|
315
|
-
#
|
340
|
+
# Remove any Formtastic-specific options before passing the down options.
|
341
|
+
#
|
316
342
|
def set_options(options)
|
317
|
-
|
318
|
-
|
319
|
-
opts.delete(key)
|
320
|
-
end
|
321
|
-
opts.merge(:object => @object)
|
343
|
+
options.except(:value_method, :label_method, :collection, :required, :label,
|
344
|
+
:as, :hint, :input_html, :label_html, :value_as_class)
|
322
345
|
end
|
323
346
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
347
|
+
# Create a default button text. If the form is working with a object, it
|
348
|
+
# defaults to "Create model" or "Save model" depending if we are working
|
349
|
+
# with a new_record or not.
|
350
|
+
#
|
351
|
+
# When not working with models, it defaults to "Submit object".
|
352
|
+
#
|
353
|
+
def save_or_create_button_text(prefix='Submit') #:nodoc:
|
354
|
+
if @object
|
355
|
+
prefix = @object.new_record? ? 'Create' : 'Save'
|
356
|
+
object_name = @object.class.human_name
|
357
|
+
else
|
358
|
+
object_name = @object_name.to_s.send(@@label_str_method)
|
359
|
+
end
|
360
|
+
|
361
|
+
I18n.t(prefix.downcase, :default => prefix, :scope => [:formtastic]) << ' ' << object_name
|
329
362
|
end
|
330
363
|
|
331
364
|
# Determins if the attribute (eg :title) should be considered required or not.
|
332
365
|
#
|
333
366
|
# * if the :required option was provided in the options hash, the true/false value will be
|
334
367
|
# returned immediately, allowing the view to override any guesswork that follows:
|
368
|
+
#
|
335
369
|
# * if the :required option isn't provided in the options hash, and the ValidationReflection
|
336
370
|
# plugin is installed (http://github.com/redinger/validation_reflection), true is returned
|
337
371
|
# if the validates_presence_of macro has been used in the class for this attribute, or false
|
338
372
|
# otherwise.
|
373
|
+
#
|
339
374
|
# * if the :required option isn't provided, and the plugin isn't available, the value of the
|
340
375
|
# configuration option @@all_fields_required_by_default is used.
|
376
|
+
#
|
341
377
|
def method_required?(attribute, required_option) #:nodoc:
|
342
378
|
return required_option unless required_option.nil?
|
343
379
|
|
344
|
-
if @object.class.respond_to?(:reflect_on_all_validations)
|
380
|
+
if @object && @object.class.respond_to?(:reflect_on_all_validations)
|
345
381
|
attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
|
382
|
+
|
346
383
|
@object.class.reflect_on_all_validations.any? do |validation|
|
347
384
|
validation.macro == :validates_presence_of && validation.name == attribute_sym
|
348
385
|
end
|
@@ -351,6 +388,19 @@ module Formtastic #:nodoc:
|
|
351
388
|
end
|
352
389
|
end
|
353
390
|
|
391
|
+
# A method that deals with most of inputs (:string, :password, :file,
|
392
|
+
# :textarea and :numeric). :select, :radio, :boolean and :datetime inputs
|
393
|
+
# are not handled by this method, since they need more detailed approach.
|
394
|
+
#
|
395
|
+
# If input_html is given as option, it's passed down to the input.
|
396
|
+
#
|
397
|
+
def input_simple(type, method, options)
|
398
|
+
html_options = options.delete(:input_html) || {}
|
399
|
+
html_options = default_string_options(method).merge(html_options) if STRING_MAPPINGS.include?(type)
|
400
|
+
|
401
|
+
input_label(method, options.delete(:label), options.slice(:required)) + send(INPUT_MAPPINGS[type], method, html_options)
|
402
|
+
end
|
403
|
+
|
354
404
|
# Outputs a label and a select box containing options from the parent
|
355
405
|
# (belongs_to, has_many, has_and_belongs_to_many) association. If an association
|
356
406
|
# is has_many or has_and_belongs_to_many the select box will be set as multi-select
|
@@ -427,32 +477,23 @@ module Formtastic #:nodoc:
|
|
427
477
|
#
|
428
478
|
# Examples:
|
429
479
|
#
|
430
|
-
# f.input :authors, :
|
480
|
+
# f.input :authors, :input_html => {:size => 20, :multiple => true}
|
481
|
+
#
|
431
482
|
def select_input(method, options)
|
432
|
-
|
433
|
-
|
434
|
-
options[:value_method] ||= :id
|
435
|
-
options[:input_html] ||= {}
|
436
|
-
|
437
|
-
if (reflection = find_reflection(method)) && reflection.macro != :belongs_to
|
438
|
-
options[:input_html][:multiple] ||= true
|
439
|
-
options[:input_html][:size] ||= 5
|
440
|
-
end
|
483
|
+
collection = find_collection_for_column(method, options)
|
484
|
+
html_options = options.delete(:input_html) || {}
|
441
485
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
486
|
+
reflection = find_reflection(method)
|
487
|
+
if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
|
488
|
+
html_options[:multiple] ||= true
|
489
|
+
html_options[:size] ||= 5
|
490
|
+
end
|
447
491
|
|
448
|
-
|
449
|
-
(
|
450
|
-
|
451
|
-
|
452
|
-
def formatted_collection(collection, label_method, value_method = :id) #:nodoc:
|
453
|
-
return collection if (collection.instance_of?(Hash) || (collection.instance_of?(Array) && collection.first.instance_of?(String)))
|
454
|
-
collection.map { |o| [o.send(label_method), o.send(value_method)] }
|
492
|
+
input_name = generate_association_input_name(method)
|
493
|
+
input_label(input_name, options.delete(:label), options.slice(:required)) +
|
494
|
+
self.select(input_name, collection, set_options(options), html_options)
|
455
495
|
end
|
496
|
+
alias :boolean_select_input :select_input
|
456
497
|
|
457
498
|
# Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
|
458
499
|
# items, one for each possible choice in the belongs_to association. Each li contains a
|
@@ -500,69 +541,31 @@ module Formtastic #:nodoc:
|
|
500
541
|
# f.input :author, :as => :radio, :label_method => :label
|
501
542
|
#
|
502
543
|
# Finally, you can set :value_as_class => true if you want that LI wrappers
|
503
|
-
# contains a class with the wrapped radio input value.
|
504
|
-
# <tt>boolean_radio_input</tt> and you can see an example there.
|
544
|
+
# contains a class with the wrapped radio input value.
|
505
545
|
#
|
506
546
|
def radio_input(method, options)
|
507
|
-
|
508
|
-
|
547
|
+
collection = find_collection_for_column(method, options)
|
548
|
+
html_options = set_options(options).merge(options.delete(:input_html) || {})
|
509
549
|
|
510
550
|
input_name = generate_association_input_name(method)
|
511
551
|
value_as_class = options.delete(:value_as_class)
|
512
552
|
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
template.content_tag(:ol,
|
517
|
-
choices.map { |c|
|
518
|
-
label = (!c.instance_of?(String)) ? c.first : c
|
519
|
-
value = (!c.instance_of?(String)) ? c.last : c
|
520
|
-
|
521
|
-
li_content = template.content_tag(:label,
|
522
|
-
"#{template.radio_button(@object_name, input_name, value, set_options(options))} #{label}",
|
523
|
-
:for => generate_html_id(input_name, value.to_s.downcase)
|
524
|
-
)
|
525
|
-
|
526
|
-
li_options = value_as_class ? { :class => value.to_s.downcase } : {}
|
527
|
-
template.content_tag(:li, li_content, li_options)
|
528
|
-
}
|
529
|
-
)
|
530
|
-
)
|
531
|
-
end
|
532
|
-
|
533
|
-
# Outputs a label and a password input, nothing fancy.
|
534
|
-
def password_input(method, options)
|
535
|
-
input_label(method, options) +
|
536
|
-
template.password_field(@object_name, method, default_string_options(method))
|
537
|
-
end
|
538
|
-
|
539
|
-
|
540
|
-
# Outputs a label and a textarea, nothing fancy.
|
541
|
-
def text_input(method, options)
|
542
|
-
input_label(method, options) + template.text_area(@object_name, method, set_options(options))
|
543
|
-
end
|
544
|
-
|
545
|
-
|
546
|
-
# Outputs a label and a text input, nothing fancy, but it does pick up some attributes like
|
547
|
-
# size and maxlength -- see default_string_options() for the low-down.
|
548
|
-
def string_input(method, options)
|
549
|
-
input_label(method, options) +
|
550
|
-
template.text_field(@object_name, method, default_string_options(method))
|
551
|
-
end
|
553
|
+
list_item_content = collection.map do |c|
|
554
|
+
label = c.is_a?(Array) ? c.first : c
|
555
|
+
value = c.is_a?(Array) ? c.last : c
|
552
556
|
|
557
|
+
li_content = template.content_tag(:label,
|
558
|
+
"#{self.radio_button(input_name, value, html_options)} #{label}",
|
559
|
+
:for => generate_html_id(input_name, value.to_s.downcase)
|
560
|
+
)
|
553
561
|
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
template.text_field(@object_name, method, default_string_options(method))
|
558
|
-
end
|
562
|
+
li_options = value_as_class ? { :class => value.to_s.downcase } : {}
|
563
|
+
template.content_tag(:li, li_content, li_options)
|
564
|
+
end
|
559
565
|
|
560
|
-
|
561
|
-
def file_input(method, options)
|
562
|
-
input_label(method, options) +
|
563
|
-
template.file_field(@object_name, method, set_options(options))
|
566
|
+
field_set_and_list_wrapping_for_method(method, options, list_item_content)
|
564
567
|
end
|
565
|
-
|
568
|
+
alias :boolean_radio_input :radio_input
|
566
569
|
|
567
570
|
# Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
|
568
571
|
# items (li), one for each fragment for the date (year, month, day). Each li contains a label
|
@@ -629,118 +632,97 @@ module Formtastic #:nodoc:
|
|
629
632
|
#
|
630
633
|
def date_or_datetime_input(method, options)
|
631
634
|
position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
|
632
|
-
inputs
|
635
|
+
inputs = options.delete(:order) || I18n.translate(:'date.order') || [:year, :month, :day]
|
636
|
+
|
633
637
|
time_inputs = [:hour, :minute]
|
634
638
|
time_inputs << [:second] if options[:include_seconds]
|
635
639
|
|
640
|
+
list_items_capture = ""
|
641
|
+
|
636
642
|
# Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
|
637
|
-
datetime
|
643
|
+
datetime = @object ? @object.send(method) : nil
|
644
|
+
html_options = options.delete(:input_html) || {}
|
638
645
|
|
639
|
-
list_items_capture = ""
|
640
646
|
(inputs + time_inputs).each do |input|
|
641
|
-
html_id
|
647
|
+
html_id = generate_html_id(method, "#{position[input]}i")
|
648
|
+
field_name = "#{method}(#{position[input]}i)"
|
642
649
|
|
643
|
-
if options["discard_#{input}".intern]
|
650
|
+
list_items_capture << if options["discard_#{input}".intern]
|
644
651
|
break if time_inputs.include?(input)
|
652
|
+
|
645
653
|
hidden_value = datetime.respond_to?(input) ? datetime.send(input) : datetime
|
646
|
-
|
654
|
+
template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => html_id)
|
647
655
|
else
|
648
|
-
opts = set_options(options).merge(:prefix => @object_name, :field_name =>
|
649
|
-
item_label_text = I18n.t(input.to_s, :default => input.to_s, :scope => [:
|
650
|
-
|
656
|
+
opts = set_options(options).merge(:prefix => @object_name, :field_name => field_name)
|
657
|
+
item_label_text = I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
|
658
|
+
|
659
|
+
template.content_tag(:li,
|
651
660
|
template.content_tag(:label, item_label_text, :for => html_id) +
|
652
|
-
template.send("select_#{input}".intern,
|
661
|
+
template.send("select_#{input}".intern, datetime, opts, html_options.merge(:id => html_id))
|
653
662
|
)
|
654
663
|
end
|
655
664
|
end
|
656
665
|
|
657
|
-
|
658
|
-
%{<legend><span>#{label_text(method, options)}</span></legend>} +
|
659
|
-
template.content_tag(:ol, list_items_capture)
|
660
|
-
)
|
666
|
+
field_set_and_list_wrapping_for_method(method, options, list_items_capture)
|
661
667
|
end
|
662
668
|
|
663
|
-
# Outputs a label containing a checkbox and the label text.
|
664
|
-
# name (method name) and can be altered with the :label option.
|
669
|
+
# Outputs a label containing a checkbox and the label text. The label defaults
|
670
|
+
# to the column name (method name) and can be altered with the :label option.
|
671
|
+
#
|
672
|
+
# Different from other inputs, :required options has no effect here and
|
673
|
+
# :checked_value and :unchecked_value options are also available.
|
674
|
+
#
|
665
675
|
def boolean_input(method, options)
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
676
|
+
html_options = options.delete(:input_html) || {}
|
677
|
+
|
678
|
+
content = self.check_box(method, set_options(options).merge(html_options),
|
679
|
+
options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
|
680
|
+
|
681
|
+
# required does not make sense in check box
|
682
|
+
input_label(method, content + options.delete(:label), :skip_required => true)
|
670
683
|
end
|
671
684
|
|
672
|
-
#
|
673
|
-
# text defaults to "Yes" and "No" respectively, but can be altered with the :true and :false
|
674
|
-
# options. The label text to the column name (method name), but can be altered with the
|
675
|
-
# :label option. Example:
|
676
|
-
#
|
677
|
-
# f.input :awesome, :as => :boolean_select, :true => "Yeah!", :false => "Nah!", :label => "Make this sucker public?"
|
685
|
+
# Generates an input for the given method using the type supplied with :as.
|
678
686
|
#
|
679
|
-
#
|
687
|
+
# If the input is included in INPUT_MAPPINGS, it uses input_simple
|
688
|
+
# implementation which maps most of the inputs. All others have specific
|
689
|
+
# code and then a proper handler should be called (like radio_input) for
|
690
|
+
# :radio types.
|
680
691
|
#
|
681
|
-
|
682
|
-
|
683
|
-
# Make this sucker public?<abbr title="required">*</abbr>
|
684
|
-
# </label>
|
685
|
-
# <select id="post_public" name="post[public]">
|
686
|
-
# <option value="1">Yeah!</option>
|
687
|
-
# <option value="0">Nah!</option>
|
688
|
-
# </select>
|
689
|
-
# </li>
|
690
|
-
#
|
691
|
-
def boolean_select_input(method, options)
|
692
|
-
options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic]).send(@@label_str_method)
|
693
|
-
options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic]).send(@@label_str_method)
|
692
|
+
def inline_input_for(method, options)
|
693
|
+
input_type = options.delete(:as)
|
694
694
|
|
695
|
-
|
696
|
-
|
695
|
+
if INPUT_MAPPINGS.key?(input_type)
|
696
|
+
input_simple(input_type, method, options)
|
697
|
+
else
|
698
|
+
send("#{input_type}_input", method, options)
|
699
|
+
end
|
697
700
|
end
|
698
701
|
|
699
|
-
#
|
700
|
-
#
|
701
|
-
# altered with the :true and :false options. The fieldset legend defaults to the column name
|
702
|
-
# (method name), but can be altered with the :label option. Example:
|
703
|
-
#
|
704
|
-
# f.input :awesome, :as => :boolean_radio, :true => "Yeah!", :false => "Nah!", :label => "Awesome?"
|
705
|
-
#
|
706
|
-
# Returns something like:
|
702
|
+
# Generates error messages for the given method. Errors can be shown as list
|
703
|
+
# or as sentence. If :none is set, no error is shown.
|
707
704
|
#
|
708
|
-
|
709
|
-
|
710
|
-
# <ol>
|
711
|
-
# <li class="true">
|
712
|
-
# <label for="post_public_true">
|
713
|
-
# <input id="post_public_true" name="post[public]" type="radio" value="true" /> Yeah!
|
714
|
-
# </label>
|
715
|
-
# </li>
|
716
|
-
# <li class="false">
|
717
|
-
# <label for="post_public_false">
|
718
|
-
# <input id="post_public_false" name="post[public]" type="radio" checked="checked" /> Nah!
|
719
|
-
# </label>
|
720
|
-
# </li>
|
721
|
-
# </ol>
|
722
|
-
# </fieldset>
|
723
|
-
# </li>
|
724
|
-
#
|
725
|
-
def boolean_radio_input(method, options)
|
726
|
-
options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic]).send(@@label_str_method)
|
727
|
-
options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic]).send(@@label_str_method)
|
705
|
+
def inline_errors_for(method, options) #:nodoc:
|
706
|
+
return nil unless @object && [:sentence, :list].include?(@@inline_errors)
|
728
707
|
|
729
|
-
|
730
|
-
|
708
|
+
errors = @object.errors.on(method.to_s).to_a
|
709
|
+
send("error_#{@@inline_errors}", errors) unless errors.empty?
|
731
710
|
end
|
732
711
|
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
end
|
712
|
+
# Generates hints for the given method using the text supplied in :hint.
|
713
|
+
#
|
714
|
+
def inline_hints_for(method, options) #:nodoc:
|
715
|
+
options[:hint].blank? ? '' : template.content_tag(:p, options[:hint], :class => 'inline-hints')
|
738
716
|
end
|
739
717
|
|
718
|
+
# Creates an error sentence by calling to_sentence on the errors array.
|
719
|
+
#
|
740
720
|
def error_sentence(errors) #:nodoc:
|
741
721
|
template.content_tag(:p, errors.to_sentence, :class => 'inline-errors')
|
742
722
|
end
|
743
723
|
|
724
|
+
# Creates an error li list.
|
725
|
+
#
|
744
726
|
def error_list(errors) #:nodoc:
|
745
727
|
list_elements = []
|
746
728
|
errors.each do |error|
|
@@ -749,19 +731,24 @@ module Formtastic #:nodoc:
|
|
749
731
|
template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
|
750
732
|
end
|
751
733
|
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
def
|
757
|
-
|
758
|
-
end
|
734
|
+
# Generates the label for the input. Accepts the same options as Rails label
|
735
|
+
# method and a fourth option that allows the label to be generated as span
|
736
|
+
# with class label.
|
737
|
+
#
|
738
|
+
def input_label(method, text, options={}, as_span=false) #:nodoc:
|
739
|
+
text << required_or_optional_string(options.delete(:required)) unless options.delete(:skip_required)
|
759
740
|
|
760
|
-
|
761
|
-
|
762
|
-
|
741
|
+
if as_span
|
742
|
+
options[:class] ||= 'label'
|
743
|
+
template.content_tag(:span, text, options)
|
744
|
+
else
|
745
|
+
self.label(method, text, options)
|
746
|
+
end
|
763
747
|
end
|
764
748
|
|
749
|
+
# Generates the required or optional string. If the value set is a proc,
|
750
|
+
# it evaluates the proc first.
|
751
|
+
#
|
765
752
|
def required_or_optional_string(required) #:nodoc:
|
766
753
|
string_or_proc = required ? @@required_string : @@optional_string
|
767
754
|
|
@@ -772,24 +759,46 @@ module Formtastic #:nodoc:
|
|
772
759
|
end
|
773
760
|
end
|
774
761
|
|
775
|
-
|
776
|
-
|
762
|
+
# Generates a fieldset and wraps the content in an ordered list. When working
|
763
|
+
# with nested attributes (in Rails 2.3), it allows %i as interpolation option
|
764
|
+
# in :name. So you can do:
|
765
|
+
#
|
766
|
+
# f.inputs :name => 'Task #%i', :for => :tasks
|
767
|
+
#
|
768
|
+
# And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
|
769
|
+
# 'Task #3' and so on.
|
770
|
+
#
|
771
|
+
# If you are using inputs :for, for more than one association in the same
|
772
|
+
# form builder, you might want to set the nested_child_index as well. You
|
773
|
+
# can do that doing:
|
774
|
+
#
|
775
|
+
# f.nested_child_index = -1
|
776
|
+
#
|
777
|
+
def field_set_and_list_wrapping(html_options, contents = '', &block) #:nodoc:
|
778
|
+
# Generates the legend text allowing nested_child_index support for interpolation
|
779
|
+
legend_text = html_options.delete(:name).to_s
|
780
|
+
legend_text %= html_options[:parent_builder].instance_variable_get('@nested_child_index').to_i + 1
|
781
|
+
|
777
782
|
legend = legend_text.blank? ? "" : template.content_tag(:legend, template.content_tag(:span, legend_text))
|
778
|
-
if block_given?
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
)
|
785
|
-
)
|
786
|
-
else
|
787
|
-
template.content_tag(:fieldset,
|
788
|
-
legend + template.content_tag(:ol, contents),
|
789
|
-
field_set_html_options
|
790
|
-
)
|
791
|
-
end
|
783
|
+
contents = template.capture(&block) if block_given?
|
784
|
+
|
785
|
+
fieldset = template.content_tag(:fieldset,
|
786
|
+
legend + template.content_tag(:ol, contents),
|
787
|
+
html_options.except(:builder, :parent_builder)
|
788
|
+
)
|
792
789
|
|
790
|
+
template.concat(fieldset) if block_given?
|
791
|
+
fieldset
|
792
|
+
end
|
793
|
+
|
794
|
+
# Also generates a fieldset and an ordered list but with label based in
|
795
|
+
# method. This methods is currently used by radio and datetime inputs.
|
796
|
+
#
|
797
|
+
def field_set_and_list_wrapping_for_method(method, options, contents)
|
798
|
+
template.content_tag(:fieldset,
|
799
|
+
%{<legend>#{input_label(method, options.delete(:label), options.slice(:required), true)}</legend>} +
|
800
|
+
template.content_tag(:ol, contents)
|
801
|
+
)
|
793
802
|
end
|
794
803
|
|
795
804
|
# For methods that have a database column, take a best guess as to what the inout method
|
@@ -799,46 +808,99 @@ module Formtastic #:nodoc:
|
|
799
808
|
#
|
800
809
|
# If there is no column for the method (eg "virtual columns" with an attr_accessor), the
|
801
810
|
# default is a :string, a similar behaviour to Rails' scaffolding.
|
802
|
-
|
811
|
+
#
|
812
|
+
def default_input_type(method) #:nodoc:
|
813
|
+
return :string if @object.nil?
|
814
|
+
|
803
815
|
# Find the column object by attribute
|
804
|
-
column = object.column_for_attribute(method) if object.respond_to?(:column_for_attribute)
|
805
|
-
|
806
|
-
|
816
|
+
column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
|
817
|
+
|
818
|
+
# Associations map by default to a select
|
819
|
+
return :select if column.nil? && find_reflection(method)
|
807
820
|
|
808
821
|
if column
|
809
822
|
# handle the special cases where the column type doesn't map to an input method
|
810
|
-
return :select
|
811
|
-
return :select if column.type == :integer && method.to_s =~ /_id$/
|
823
|
+
return :select if column.type == :integer && method.to_s =~ /_id$/
|
812
824
|
return :datetime if column.type == :timestamp
|
813
|
-
return :numeric
|
825
|
+
return :numeric if [:integer, :float, :decimal].include?(column.type)
|
814
826
|
return :password if column.type == :string && method.to_s =~ /password/
|
815
827
|
# otherwise assume the input name will be the same as the column type (eg string_input)
|
816
828
|
return column.type
|
817
829
|
else
|
818
|
-
obj = object.send(method)
|
819
|
-
|
830
|
+
obj = @object.send(method) if @object.respond_to?(method)
|
831
|
+
|
832
|
+
return :file if obj && @@file_methods.any? { |m| obj.respond_to?(m) }
|
820
833
|
return :password if method.to_s =~ /password/
|
821
834
|
return :string
|
822
835
|
end
|
823
836
|
end
|
824
837
|
|
825
|
-
# Used by
|
826
|
-
#
|
827
|
-
#
|
828
|
-
|
829
|
-
|
830
|
-
|
838
|
+
# Used by select and radio inputs. The collection can be retrieved by
|
839
|
+
# three ways:
|
840
|
+
#
|
841
|
+
# * Explicitly provided through :collection
|
842
|
+
# * Retrivied through an association
|
843
|
+
# * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
|
844
|
+
#
|
845
|
+
# If the collection is not a hash or an array of strings, fixnums or arrays,
|
846
|
+
# we use label_method and value_method to retreive an array with the
|
847
|
+
# appropriate label and value.
|
848
|
+
#
|
849
|
+
def find_collection_for_column(column, options)
|
850
|
+
reflection = find_reflection(column)
|
851
|
+
|
852
|
+
collection = if options[:collection]
|
853
|
+
options.delete(:collection)
|
854
|
+
elsif reflection || column.to_s =~ /_id$/
|
855
|
+
parent_class = if reflection
|
856
|
+
reflection.klass
|
857
|
+
else
|
858
|
+
::ActiveSupport::Deprecation.warn("The _id way of doing things is deprecated. Please use the association method (#{column.to_s.sub(/_id$/,'')})", caller[3..-1])
|
859
|
+
column.to_s.sub(/_id$/,'').camelize.constantize
|
860
|
+
end
|
861
|
+
|
862
|
+
parent_class.find(:all)
|
831
863
|
else
|
832
|
-
|
833
|
-
column.to_s.sub(/_id$/,'').camelize.constantize
|
864
|
+
create_boolean_collection(options)
|
834
865
|
end
|
835
|
-
|
866
|
+
|
867
|
+
collection = collection.to_a if collection.instance_of?(Hash)
|
868
|
+
|
869
|
+
# Return if we have an Array of strings, fixnums or arrays
|
870
|
+
return collection if collection.instance_of?(Array) &&
|
871
|
+
[Array, Fixnum, String, Symbol].include?(collection.first.class)
|
872
|
+
|
873
|
+
label = options.delete(:label_method) || detect_label_method(collection)
|
874
|
+
value = options.delete(:value_method) || :id
|
875
|
+
|
876
|
+
collection.map { |o| [o.send(label), o.send(value)] }
|
836
877
|
end
|
837
878
|
|
838
|
-
#
|
839
|
-
#
|
840
|
-
#
|
841
|
-
|
879
|
+
# Detected the label collection method when none is supplied using the
|
880
|
+
# values set in @@collection_label_methods.
|
881
|
+
#
|
882
|
+
def detect_label_method(collection) #:nodoc:
|
883
|
+
@@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
|
884
|
+
end
|
885
|
+
|
886
|
+
# Returns a hash to be used by radio and select inputs when a boolean field
|
887
|
+
# is provided.
|
888
|
+
#
|
889
|
+
def create_boolean_collection(options)
|
890
|
+
options[:true] ||= I18n.t('yes', :default => 'Yes', :scope => [:formtastic])
|
891
|
+
options[:false] ||= I18n.t('no', :default => 'No', :scope => [:formtastic])
|
892
|
+
options[:value_as_class] = true unless options.key?(:value_as_class)
|
893
|
+
|
894
|
+
{ options.delete(:true) => true, options.delete(:false) => false }
|
895
|
+
end
|
896
|
+
|
897
|
+
# Used by association inputs (select, radio) to generate the name that should
|
898
|
+
# be used for the input
|
899
|
+
#
|
900
|
+
# belongs_to :author; f.input :author; will generate 'author_id'
|
901
|
+
# has_many :authors; f.input :authors; will generate 'author_ids'
|
902
|
+
# has_and_belongs_to_many will act like has_many
|
903
|
+
#
|
842
904
|
def generate_association_input_name(method)
|
843
905
|
if reflection = find_reflection(method)
|
844
906
|
method = "#{method.to_s.singularize}_id"
|
@@ -847,25 +909,24 @@ module Formtastic #:nodoc:
|
|
847
909
|
method
|
848
910
|
end
|
849
911
|
|
850
|
-
# If an association method is passed in (f.input :author) try to find the
|
912
|
+
# If an association method is passed in (f.input :author) try to find the
|
913
|
+
# reflection object.
|
914
|
+
#
|
851
915
|
def find_reflection(method)
|
852
916
|
object.class.reflect_on_association(method) if object.class.respond_to?(:reflect_on_association)
|
853
917
|
end
|
854
918
|
|
919
|
+
# Generates default_string_options by retrieving column information from
|
920
|
+
# the database.
|
921
|
+
#
|
855
922
|
def default_string_options(method) #:nodoc:
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
column = @object.column_for_attribute(method)
|
860
|
-
rescue NoMethodError
|
861
|
-
column = nil
|
862
|
-
end
|
863
|
-
opts = if column.nil? || column.limit.nil?
|
923
|
+
column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
|
924
|
+
|
925
|
+
if column.nil? || column.limit.nil?
|
864
926
|
{ :size => @@default_text_field_size }
|
865
927
|
else
|
866
928
|
{ :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
|
867
929
|
end
|
868
|
-
set_options(opts)
|
869
930
|
end
|
870
931
|
|
871
932
|
# Generate the html id for the li tag.
|
@@ -931,6 +992,7 @@ module Formtastic #:nodoc:
|
|
931
992
|
# Please note: Although it's possible to call Rails' built-in form_for() helper without an
|
932
993
|
# object, all semantic forms *must* have an object (either Post.new or @post), as Formtastic
|
933
994
|
# has too many dependencies on an ActiveRecord object being present.
|
995
|
+
#
|
934
996
|
module SemanticFormHelper
|
935
997
|
@@builder = Formtastic::SemanticFormBuilder
|
936
998
|
|