tmayad-formtastic 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.textile +517 -0
  3. data/Rakefile +101 -0
  4. data/generators/form/USAGE +16 -0
  5. data/generators/form/form_generator.rb +120 -0
  6. data/generators/form/templates/view__form.html.erb +5 -0
  7. data/generators/form/templates/view__form.html.haml +4 -0
  8. data/generators/formtastic/formtastic_generator.rb +24 -0
  9. data/generators/formtastic/templates/formtastic.css +138 -0
  10. data/generators/formtastic/templates/formtastic.rb +54 -0
  11. data/generators/formtastic/templates/formtastic_changes.css +10 -0
  12. data/generators/formtastic_stylesheets/formtastic_stylesheets_generator.rb +16 -0
  13. data/lib/formtastic.rb +1643 -0
  14. data/lib/formtastic/i18n.rb +32 -0
  15. data/lib/locale/en.yml +8 -0
  16. data/rails/init.rb +3 -0
  17. data/spec/buttons_spec.rb +149 -0
  18. data/spec/commit_button_spec.rb +344 -0
  19. data/spec/custom_builder_spec.rb +62 -0
  20. data/spec/custom_macros.rb +561 -0
  21. data/spec/defaults_spec.rb +20 -0
  22. data/spec/error_proc_spec.rb +27 -0
  23. data/spec/errors_spec.rb +85 -0
  24. data/spec/form_helper_spec.rb +120 -0
  25. data/spec/i18n_spec.rb +131 -0
  26. data/spec/include_blank_spec.rb +70 -0
  27. data/spec/input_spec.rb +608 -0
  28. data/spec/inputs/boolean_input_spec.rb +93 -0
  29. data/spec/inputs/check_boxes_input_spec.rb +162 -0
  30. data/spec/inputs/country_input_spec.rb +80 -0
  31. data/spec/inputs/date_input_spec.rb +45 -0
  32. data/spec/inputs/datetime_input_spec.rb +155 -0
  33. data/spec/inputs/file_input_spec.rb +33 -0
  34. data/spec/inputs/hidden_input_spec.rb +52 -0
  35. data/spec/inputs/numeric_input_spec.rb +44 -0
  36. data/spec/inputs/password_input_spec.rb +46 -0
  37. data/spec/inputs/radio_input_spec.rb +149 -0
  38. data/spec/inputs/select_input_spec.rb +459 -0
  39. data/spec/inputs/string_input_spec.rb +47 -0
  40. data/spec/inputs/text_input_spec.rb +33 -0
  41. data/spec/inputs/time_input_spec.rb +44 -0
  42. data/spec/inputs/time_zone_input_spec.rb +102 -0
  43. data/spec/inputs_spec.rb +395 -0
  44. data/spec/label_spec.rb +48 -0
  45. data/spec/semantic_fields_for_spec.rb +44 -0
  46. data/spec/spec.opts +2 -0
  47. data/spec/spec_helper.rb +212 -0
  48. metadata +169 -0
@@ -0,0 +1,10 @@
1
+ /* -------------------------------------------------------------------------------------------------
2
+
3
+ Load this stylesheet after formtastic.css in your layouts to override the CSS to suit your needs.
4
+ This will allow you to update formtastic.css with new releases without clobbering your own changes.
5
+
6
+ For example, to make the inline hint paragraphs a little darker in color than the standard #666:
7
+
8
+ form.formtastic fieldset ol li p.inline-hints { color:#333; }
9
+
10
+ --------------------------------------------------------------------------------------------------*/
@@ -0,0 +1,16 @@
1
+ class FormtasticStylesheetsGenerator < Rails::Generator::Base
2
+
3
+ def initialize(*runtime_args)
4
+ puts %q{
5
+ ===================================================
6
+ Please run `./script/generate formtastic` instead.
7
+ ===================================================
8
+ }
9
+ end
10
+
11
+ def manifest
12
+ record do |m|
13
+ end
14
+ end
15
+
16
+ end
data/lib/formtastic.rb ADDED
@@ -0,0 +1,1643 @@
1
+ # coding: utf-8
2
+ require File.join(File.dirname(__FILE__), *%w[formtastic i18n])
3
+
4
+ module Formtastic #:nodoc:
5
+
6
+ class SemanticFormBuilder < ActionView::Helpers::FormBuilder
7
+
8
+ @@default_text_field_size = 50
9
+ @@default_text_area_height = 20
10
+ @@all_fields_required_by_default = true
11
+ @@include_blank_for_select_by_default = true
12
+ @@required_string = proc { %{<abbr title="#{::Formtastic::I18n.t(:required)}">*</abbr>} }
13
+ @@optional_string = ''
14
+ @@inline_errors = :sentence
15
+ @@label_str_method = :humanize
16
+ @@collection_label_methods = %w[to_label display_name full_name name title username login value to_s]
17
+ @@inline_order = [ :input, :hints, :errors ]
18
+ @@file_methods = [ :file?, :public_filename ]
19
+ @@priority_countries = ["Australia", "Canada", "United Kingdom", "United States"]
20
+ @@i18n_lookups_by_default = false
21
+ @@default_commit_button_accesskey = nil
22
+
23
+ cattr_accessor :default_text_field_size, :default_text_area_height, :all_fields_required_by_default, :include_blank_for_select_by_default,
24
+ :required_string, :optional_string, :inline_errors, :label_str_method, :collection_label_methods,
25
+ :inline_order, :file_methods, :priority_countries, :i18n_lookups_by_default, :default_commit_button_accesskey
26
+
27
+ RESERVED_COLUMNS = [:created_at, :updated_at, :created_on, :updated_on, :lock_version, :version]
28
+
29
+ INLINE_ERROR_TYPES = [:sentence, :list, :first]
30
+
31
+ attr_accessor :template
32
+
33
+ # Returns a suitable form input for the given +method+, using the database column information
34
+ # and other factors (like the method name) to figure out what you probably want.
35
+ #
36
+ # Options:
37
+ #
38
+ # * :as - override the input type (eg force a :string to render as a :password field)
39
+ # * :label - use something other than the method name as the label text, when false no label is printed
40
+ # * :required - specify if the column is required (true) or not (false)
41
+ # * :hint - provide some text to hint or help the user provide the correct information for a field
42
+ # * :input_html - provide options that will be passed down to the generated input
43
+ # * :wrapper_html - provide options that will be passed down to the li wrapper
44
+ #
45
+ # Input Types:
46
+ #
47
+ # Most inputs map directly to one of ActiveRecord's column types by default (eg string_input),
48
+ # but there are a few special cases and some simplification (:integer, :float and :decimal
49
+ # columns all map to a single numeric_input, for example).
50
+ #
51
+ # * :select (a select menu for associations) - default to association names
52
+ # * :check_boxes (a set of check_box inputs for associations) - alternative to :select has_many and has_and_belongs_to_many associations
53
+ # * :radio (a set of radio inputs for associations) - alternative to :select belongs_to associations
54
+ # * :time_zone (a select menu with time zones)
55
+ # * :password (a password input) - default for :string column types with 'password' in the method name
56
+ # * :text (a textarea) - default for :text column types
57
+ # * :date (a date select) - default for :date column types
58
+ # * :datetime (a date and time select) - default for :datetime and :timestamp column types
59
+ # * :time (a time select) - default for :time column types
60
+ # * :boolean (a checkbox) - default for :boolean column types (you can also have booleans as :select and :radio)
61
+ # * :string (a text field) - default for :string column types
62
+ # * :numeric (a text field, like string) - default for :integer, :float and :decimal column types
63
+ # * :country (a select menu of country names) - requires a country_select plugin to be installed
64
+ # * :hidden (a hidden field) - creates a hidden field (added for compatibility)
65
+ #
66
+ # Example:
67
+ #
68
+ # <% semantic_form_for @employee do |form| %>
69
+ # <% form.inputs do -%>
70
+ # <%= form.input :secret, :value => "Hello" %>
71
+ # <%= form.input :name, :label => "Full Name" %>
72
+ # <%= form.input :manager_id, :as => :radio %>
73
+ # <%= form.input :hired_at, :as => :date, :label => "Date Hired" %>
74
+ # <%= form.input :phone, :required => false, :hint => "Eg: +1 555 1234" %>
75
+ # <% end %>
76
+ # <% end %>
77
+ #
78
+ def input(method, options = {})
79
+ options[:required] = method_required?(method) unless options.key?(:required)
80
+ options[:as] ||= default_input_type(method, options)
81
+
82
+ html_class = [ options[:as], (options[:required] ? :required : :optional) ]
83
+ html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
84
+
85
+ wrapper_html = options.delete(:wrapper_html) || {}
86
+ wrapper_html[:id] ||= generate_html_id(method)
87
+ wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
88
+
89
+ if options[:input_html] && options[:input_html][:id]
90
+ options[:label_html] ||= {}
91
+ options[:label_html][:for] ||= options[:input_html][:id]
92
+ end
93
+
94
+ input_parts = @@inline_order.dup
95
+ input_parts.delete(:errors) if options[:as] == :hidden
96
+
97
+ list_item_content = input_parts.map do |type|
98
+ send(:"inline_#{type}_for", method, options)
99
+ end.compact.join("\n")
100
+
101
+ return template.content_tag(:li, list_item_content, wrapper_html)
102
+ end
103
+
104
+ # Creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be
105
+ # called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc),
106
+ # or with a list of fields. These two examples are functionally equivalent:
107
+ #
108
+ # # With a block:
109
+ # <% semantic_form_for @post do |form| %>
110
+ # <% form.inputs do %>
111
+ # <%= form.input :title %>
112
+ # <%= form.input :body %>
113
+ # <% end %>
114
+ # <% end %>
115
+ #
116
+ # # With a list of fields:
117
+ # <% semantic_form_for @post do |form| %>
118
+ # <%= form.inputs :title, :body %>
119
+ # <% end %>
120
+ #
121
+ # # Output:
122
+ # <form ...>
123
+ # <fieldset class="inputs">
124
+ # <ol>
125
+ # <li class="string">...</li>
126
+ # <li class="text">...</li>
127
+ # </ol>
128
+ # </fieldset>
129
+ # </form>
130
+ #
131
+ # === Quick Forms
132
+ #
133
+ # When called without a block or a field list, an input is rendered for each column in the
134
+ # model's database table, just like Rails' scaffolding. You'll obviously want more control
135
+ # than this in a production application, but it's a great way to get started, then come back
136
+ # later to customise the form with a field list or a block of inputs. Example:
137
+ #
138
+ # <% semantic_form_for @post do |form| %>
139
+ # <%= form.inputs %>
140
+ # <% end %>
141
+ #
142
+ # With a few arguments:
143
+ # <% semantic_form_for @post do |form| %>
144
+ # <%= form.inputs "Post details", :title, :body %>
145
+ # <% end %>
146
+ #
147
+ # === Options
148
+ #
149
+ # All options (with the exception of :name/:title) are passed down to the fieldset as HTML
150
+ # attributes (id, class, style, etc). If provided, the :name/:title option is passed into a
151
+ # legend tag inside the fieldset.
152
+ #
153
+ # # With a block:
154
+ # <% semantic_form_for @post do |form| %>
155
+ # <% form.inputs :name => "Create a new post", :style => "border:1px;" do %>
156
+ # ...
157
+ # <% end %>
158
+ # <% end %>
159
+ #
160
+ # # With a list (the options must come after the field list):
161
+ # <% semantic_form_for @post do |form| %>
162
+ # <%= form.inputs :title, :body, :name => "Create a new post", :style => "border:1px;" %>
163
+ # <% end %>
164
+ #
165
+ # # ...or the equivalent:
166
+ # <% semantic_form_for @post do |form| %>
167
+ # <%= form.inputs "Create a new post", :title, :body, :style => "border:1px;" %>
168
+ # <% end %>
169
+ #
170
+ # === It's basically a fieldset!
171
+ #
172
+ # Instead of hard-coding fieldsets & legends into your form to logically group related fields,
173
+ # use inputs:
174
+ #
175
+ # <% semantic_form_for @post do |f| %>
176
+ # <% f.inputs do %>
177
+ # <%= f.input :title %>
178
+ # <%= f.input :body %>
179
+ # <% end %>
180
+ # <% f.inputs :name => "Advanced", :id => "advanced" do %>
181
+ # <%= f.input :created_at %>
182
+ # <%= f.input :user_id, :label => "Author" %>
183
+ # <% end %>
184
+ # <% f.inputs "Extra" do %>
185
+ # <%= f.input :update_at %>
186
+ # <% end %>
187
+ # <% end %>
188
+ #
189
+ # # Output:
190
+ # <form ...>
191
+ # <fieldset class="inputs">
192
+ # <ol>
193
+ # <li class="string">...</li>
194
+ # <li class="text">...</li>
195
+ # </ol>
196
+ # </fieldset>
197
+ # <fieldset class="inputs" id="advanced">
198
+ # <legend><span>Advanced</span></legend>
199
+ # <ol>
200
+ # <li class="datetime">...</li>
201
+ # <li class="select">...</li>
202
+ # </ol>
203
+ # </fieldset>
204
+ # <fieldset class="inputs">
205
+ # <legend><span>Extra</span></legend>
206
+ # <ol>
207
+ # <li class="datetime">...</li>
208
+ # </ol>
209
+ # </fieldset>
210
+ # </form>
211
+ #
212
+ # === Nested attributes
213
+ #
214
+ # As in Rails, you can use semantic_fields_for to nest attributes:
215
+ #
216
+ # <% semantic_form_for @post do |form| %>
217
+ # <%= form.inputs :title, :body %>
218
+ #
219
+ # <% form.semantic_fields_for :author, @bob do |author_form| %>
220
+ # <% author_form.inputs do %>
221
+ # <%= author_form.input :first_name, :required => false %>
222
+ # <%= author_form.input :last_name %>
223
+ # <% end %>
224
+ # <% end %>
225
+ # <% end %>
226
+ #
227
+ # But this does not look formtastic! This is equivalent:
228
+ #
229
+ # <% semantic_form_for @post do |form| %>
230
+ # <%= form.inputs :title, :body %>
231
+ # <% form.inputs :for => [ :author, @bob ] do |author_form| %>
232
+ # <%= author_form.input :first_name, :required => false %>
233
+ # <%= author_form.input :last_name %>
234
+ # <% end %>
235
+ # <% end %>
236
+ #
237
+ # And if you don't need to give options to your input call, you could do it
238
+ # in just one line:
239
+ #
240
+ # <% semantic_form_for @post do |form| %>
241
+ # <%= form.inputs :title, :body %>
242
+ # <%= form.inputs :first_name, :last_name, :for => @bob %>
243
+ # <% end %>
244
+ #
245
+ # Just remember that calling inputs generates a new fieldset to wrap your
246
+ # inputs. If you have two separate models, but, semantically, on the page
247
+ # they are part of the same fieldset, you should use semantic_fields_for
248
+ # instead (just as you would do with Rails' form builder).
249
+ #
250
+ def inputs(*args, &block)
251
+ title = field_set_title_from_args(*args)
252
+ html_options = args.extract_options!
253
+ html_options[:class] ||= "inputs"
254
+ html_options[:name] = title
255
+
256
+ if html_options[:for] # Nested form
257
+ inputs_for_nested_attributes(*(args << html_options), &block)
258
+ elsif block_given?
259
+ field_set_and_list_wrapping(*(args << html_options), &block)
260
+ else
261
+ if @object && args.empty?
262
+ args = self.association_columns(:belongs_to)
263
+ args += self.content_columns
264
+ args -= RESERVED_COLUMNS
265
+ args.compact!
266
+ end
267
+ legend = args.shift if args.first.is_a?(::String)
268
+ contents = args.collect { |method| input(method.to_sym) }
269
+ args.unshift(legend) if legend.present?
270
+
271
+ field_set_and_list_wrapping(*((args << html_options) << contents))
272
+ end
273
+ end
274
+ alias :input_field_set :inputs
275
+
276
+ # Creates a fieldset and ol tag wrapping for form buttons / actions as list items.
277
+ # See inputs documentation for a full example. The fieldset's default class attriute
278
+ # is set to "buttons".
279
+ #
280
+ # See inputs for html attributes and special options.
281
+ def buttons(*args, &block)
282
+ html_options = args.extract_options!
283
+ html_options[:class] ||= "buttons"
284
+
285
+ if block_given?
286
+ field_set_and_list_wrapping(html_options, &block)
287
+ else
288
+ args = [:commit] if args.empty?
289
+ contents = args.map { |button_name| send(:"#{button_name}_button") }
290
+ field_set_and_list_wrapping(html_options, contents)
291
+ end
292
+ end
293
+ alias :button_field_set :buttons
294
+
295
+ # Creates a submit input tag with the value "Save [model name]" (for existing records) or
296
+ # "Create [model name]" (for new records) by default:
297
+ #
298
+ # <%= form.commit_button %> => <input name="commit" type="submit" value="Save Post" />
299
+ #
300
+ # The value of the button text can be overridden:
301
+ #
302
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
303
+ # <%= form.commit_button :label => "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
304
+ #
305
+ # And you can pass html atributes down to the input, with or without the button text:
306
+ #
307
+ # <%= form.commit_button "Go" %> => <input name="commit" type="submit" value="Go" class="{create|update|submit}" />
308
+ # <%= form.commit_button :class => "pretty" %> => <input name="commit" type="submit" value="Save Post" class="pretty {create|update|submit}" />
309
+ #
310
+ def commit_button(*args)
311
+ options = args.extract_options!
312
+ text = options.delete(:label) || args.shift
313
+
314
+ if @object
315
+ key = @object.new_record? ? :create : :update
316
+ object_name = @object.class.human_name
317
+ else
318
+ key = :submit
319
+ object_name = @object_name.to_s.send(@@label_str_method)
320
+ end
321
+
322
+ text = (self.localized_string(key, text, :action, :model => object_name) ||
323
+ ::Formtastic::I18n.t(key, :model => object_name)) unless text.is_a?(::String)
324
+
325
+ button_html = options.delete(:button_html) || {}
326
+ button_html.merge!(:class => [button_html[:class], key].compact.join(' '))
327
+ element_class = ['commit', options.delete(:class)].compact.join(' ') # TODO: Add class reflecting on form action.
328
+ accesskey = (options.delete(:accesskey) || @@default_commit_button_accesskey) unless button_html.has_key?(:accesskey)
329
+ button_html = button_html.merge(:accesskey => accesskey) if accesskey
330
+ template.content_tag(:li, self.submit(text, button_html), :class => element_class)
331
+ end
332
+
333
+ # A thin wrapper around #fields_for to set :builder => Formtastic::SemanticFormBuilder
334
+ # for nesting forms:
335
+ #
336
+ # # Example:
337
+ # <% semantic_form_for @post do |post| %>
338
+ # <% post.semantic_fields_for :author do |author| %>
339
+ # <% author.inputs :name %>
340
+ # <% end %>
341
+ # <% end %>
342
+ #
343
+ # # Output:
344
+ # <form ...>
345
+ # <fieldset class="inputs">
346
+ # <ol>
347
+ # <li class="string"><input type='text' name='post[author][name]' id='post_author_name' /></li>
348
+ # </ol>
349
+ # </fieldset>
350
+ # </form>
351
+ #
352
+ def semantic_fields_for(record_or_name_or_array, *args, &block)
353
+ opts = args.extract_options!
354
+ opts[:builder] ||= Formtastic::SemanticFormHelper.builder
355
+ args.push(opts)
356
+ fields_for(record_or_name_or_array, *args, &block)
357
+ end
358
+
359
+ # Generates the label for the input. It also accepts the same arguments as
360
+ # Rails label method. It has three options that are not supported by Rails
361
+ # label method:
362
+ #
363
+ # * :required - Appends an abbr tag if :required is true
364
+ # * :label - An alternative form to give the label content. Whenever label
365
+ # is false, a blank string is returned.
366
+ # * :input_name - Gives the input to match for. This is needed when you want to
367
+ # to call f.label :authors but it should match :author_ids.
368
+ #
369
+ # == Examples
370
+ #
371
+ # f.label :title # like in rails, except that it searches the label on I18n API too
372
+ #
373
+ # f.label :title, "Your post title"
374
+ # f.label :title, :label => "Your post title" # Added for formtastic API
375
+ #
376
+ # f.label :title, :required => true # Returns <label>Title<abbr title="required">*</abbr></label>
377
+ #
378
+ def label(method, options_or_text=nil, options=nil)
379
+ if options_or_text.is_a?(Hash)
380
+ return "" if options_or_text[:label] == false
381
+ options = options_or_text
382
+ text = options.delete(:label)
383
+ else
384
+ text = options_or_text
385
+ options ||= {}
386
+ end
387
+ text = localized_string(method, text, :label) || humanized_attribute_name(method)
388
+ text += required_or_optional_string(options.delete(:required))
389
+
390
+ # special case for boolean (checkbox) labels, which have a nested input
391
+ text = (options.delete(:label_prefix_for_nested_input) || "") + text
392
+
393
+ input_name = options.delete(:input_name) || method
394
+ super(input_name, text, options)
395
+ end
396
+
397
+ # Generates error messages for the given method. Errors can be shown as list,
398
+ # as sentence or just the first error can be displayed. If :none is set, no error is shown.
399
+ #
400
+ # This method is also aliased as errors_on, so you can call on your custom
401
+ # inputs as well:
402
+ #
403
+ # semantic_form_for :post do |f|
404
+ # f.text_field(:body)
405
+ # f.errors_on(:body)
406
+ # end
407
+ #
408
+ def inline_errors_for(method, options = nil) #:nodoc:
409
+ if render_inline_errors?
410
+ errors = @object.errors[method.to_sym]
411
+ send(:"error_#{@@inline_errors}", [*errors]) if errors.present?
412
+ else
413
+ nil
414
+ end
415
+ end
416
+ alias :errors_on :inline_errors_for
417
+
418
+ protected
419
+
420
+ def render_inline_errors?
421
+ @object && @object.respond_to?(:errors) && INLINE_ERROR_TYPES.include?(@@inline_errors)
422
+ end
423
+
424
+ # Collects content columns (non-relation columns) for the current form object class.
425
+ #
426
+ def content_columns #:nodoc:
427
+ self.model_name.constantize.content_columns.collect { |c| c.name.to_sym }.compact rescue []
428
+ end
429
+
430
+ # Collects association columns (relation columns) for the current form object class.
431
+ #
432
+ def association_columns(*by_associations) #:nodoc:
433
+ if @object.present?
434
+ @object.class.reflections.collect do |name, _|
435
+ if by_associations.present?
436
+ name if by_associations.include?(_.macro)
437
+ else
438
+ name
439
+ end
440
+ end.compact
441
+ else
442
+ []
443
+ end
444
+ end
445
+
446
+ # Prepare options to be sent to label
447
+ #
448
+ def options_for_label(options) #:nodoc:
449
+ options.slice(:label, :required).merge!(options.fetch(:label_html, {}))
450
+ end
451
+
452
+ # Deals with :for option when it's supplied to inputs methods. Additional
453
+ # options to be passed down to :for should be supplied using :for_options
454
+ # key.
455
+ #
456
+ # It should raise an error if a block with arity zero is given.
457
+ #
458
+ def inputs_for_nested_attributes(*args, &block) #:nodoc:
459
+ options = args.extract_options!
460
+ args << options.merge!(:parent => { :builder => self, :for => options[:for] })
461
+
462
+ fields_for_block = if block_given?
463
+ raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
464
+ 'but the block does not accept any argument.' if block.arity <= 0
465
+
466
+ proc { |f| f.inputs(*args){ block.call(f) } }
467
+ else
468
+ proc { |f| f.inputs(*args) }
469
+ end
470
+
471
+ fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten
472
+ semantic_fields_for(*fields_for_args, &fields_for_block)
473
+ end
474
+
475
+ # Remove any Formtastic-specific options before passing the down options.
476
+ #
477
+ def strip_formtastic_options(options) #:nodoc:
478
+ options.except(:value_method, :label_method, :collection, :required, :label,
479
+ :as, :hint, :input_html, :label_html, :value_as_class)
480
+ end
481
+
482
+ # Determins if the attribute (eg :title) should be considered required or not.
483
+ #
484
+ # * if the :required option was provided in the options hash, the true/false value will be
485
+ # returned immediately, allowing the view to override any guesswork that follows:
486
+ #
487
+ # * if the :required option isn't provided in the options hash, and the ValidationReflection
488
+ # plugin is installed (http://github.com/redinger/validation_reflection), true is returned
489
+ # if the validates_presence_of macro has been used in the class for this attribute, or false
490
+ # otherwise.
491
+ #
492
+ # * if the :required option isn't provided, and the plugin isn't available, the value of the
493
+ # configuration option @@all_fields_required_by_default is used.
494
+ #
495
+ def method_required?(attribute) #:nodoc:
496
+ if @object && @object.class.respond_to?(:reflect_on_validations_for)
497
+ attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
498
+
499
+ @object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
500
+ validation.macro == :validates_presence_of &&
501
+ validation.name == attribute_sym &&
502
+ (validation.options.present? ? options_require_validation?(validation.options) : true)
503
+ end
504
+ else
505
+ @@all_fields_required_by_default
506
+ end
507
+ end
508
+
509
+ # Determines whether the given options evaluate to true
510
+ def options_require_validation?(options) #nodoc
511
+ if_condition = !options[:if].nil?
512
+ condition = if_condition ? options[:if] : options[:unless]
513
+
514
+ condition = if condition.respond_to?(:call)
515
+ condition.call(@object)
516
+ elsif condition.is_a?(::Symbol) && @object.respond_to?(condition)
517
+ @object.send(condition)
518
+ else
519
+ condition
520
+ end
521
+
522
+ if_condition ? !!condition : !condition
523
+ end
524
+
525
+ def basic_input_helper(form_helper_method, type, method, options) #:nodoc:
526
+ html_options = options.delete(:input_html) || {}
527
+ html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password, :text].include?(type)
528
+
529
+ self.label(method, options_for_label(options)) <<
530
+ self.send(form_helper_method, method, html_options)
531
+ end
532
+
533
+ # Outputs a label and standard Rails text field inside the wrapper.
534
+ def string_input(method, options)
535
+ basic_input_helper(:text_field, :string, method, options)
536
+ end
537
+
538
+ # Outputs a label and standard Rails password field inside the wrapper.
539
+ def password_input(method, options)
540
+ basic_input_helper(:password_field, :password, method, options)
541
+ end
542
+
543
+ # Outputs a label and standard Rails text field inside the wrapper.
544
+ def numeric_input(method, options)
545
+ basic_input_helper(:text_field, :numeric, method, options)
546
+ end
547
+
548
+ # Ouputs a label and standard Rails text area inside the wrapper.
549
+ def text_input(method, options)
550
+ basic_input_helper(:text_area, :text, method, options)
551
+ end
552
+
553
+ # Outputs a label and a standard Rails file field inside the wrapper.
554
+ def file_input(method, options)
555
+ basic_input_helper(:file_field, :file, method, options)
556
+ end
557
+
558
+ # Outputs a hidden field inside the wrapper, which should be hidden with CSS.
559
+ # Additionals options can be given and will be sent straight to hidden input
560
+ # element.
561
+ #
562
+ def hidden_input(method, options)
563
+ options ||= {}
564
+ if options[:input_html].present?
565
+ options[:value] = options[:input_html][:value] if options[:input_html][:value].present?
566
+ end
567
+ self.hidden_field(method, strip_formtastic_options(options))
568
+ end
569
+
570
+ # Outputs a label and a select box containing options from the parent
571
+ # (belongs_to, has_many, has_and_belongs_to_many) association. If an association
572
+ # is has_many or has_and_belongs_to_many the select box will be set as multi-select
573
+ # and size = 5
574
+ #
575
+ # Example (belongs_to):
576
+ #
577
+ # f.input :author
578
+ #
579
+ # <label for="book_author_id">Author</label>
580
+ # <select id="book_author_id" name="book[author_id]">
581
+ # <option value=""></option>
582
+ # <option value="1">Justin French</option>
583
+ # <option value="2">Jane Doe</option>
584
+ # </select>
585
+ #
586
+ # Example (has_many):
587
+ #
588
+ # f.input :chapters
589
+ #
590
+ # <label for="book_chapter_ids">Chapters</label>
591
+ # <select id="book_chapter_ids" name="book[chapter_ids]">
592
+ # <option value=""></option>
593
+ # <option value="1">Chapter 1</option>
594
+ # <option value="2">Chapter 2</option>
595
+ # </select>
596
+ #
597
+ # Example (has_and_belongs_to_many):
598
+ #
599
+ # f.input :authors
600
+ #
601
+ # <label for="book_author_ids">Authors</label>
602
+ # <select id="book_author_ids" name="book[author_ids]">
603
+ # <option value=""></option>
604
+ # <option value="1">Justin French</option>
605
+ # <option value="2">Jane Doe</option>
606
+ # </select>
607
+ #
608
+ #
609
+ # You can customize the options available in the select by passing in a collection (an Array or
610
+ # Hash) through the :collection option. If not provided, the choices are found by inferring the
611
+ # parent's class name from the method name and simply calling find(:all) on it
612
+ # (VehicleOwner.find(:all) in the example above).
613
+ #
614
+ # Examples:
615
+ #
616
+ # f.input :author, :collection => @authors
617
+ # f.input :author, :collection => Author.find(:all)
618
+ # f.input :author, :collection => [@justin, @kate]
619
+ # f.input :author, :collection => {@justin.name => @justin.id, @kate.name => @kate.id}
620
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
621
+ #
622
+ # The :label_method option allows you to customize the text label inside each option tag two ways:
623
+ #
624
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
625
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
626
+ #
627
+ # Examples:
628
+ #
629
+ # f.input :author, :label_method => :full_name
630
+ # f.input :author, :label_method => :login
631
+ # f.input :author, :label_method => :full_name_with_post_count
632
+ # f.input :author, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
633
+ #
634
+ # The :value_method option provides the same customization of the value attribute of each option tag.
635
+ #
636
+ # Examples:
637
+ #
638
+ # f.input :author, :value_method => :full_name
639
+ # f.input :author, :value_method => :login
640
+ # f.input :author, :value_method => Proc.new { |a| "author_#{a.login}" }
641
+ #
642
+ # You can pre-select a specific option value by passing in the :selected option.
643
+ #
644
+ # Examples:
645
+ #
646
+ # f.input :author, :selected => current_user.id
647
+ # f.input :author, :value_method => :login, :selected => current_user.login
648
+ # f.input :authors, :value_method => :login, :selected => Author.most_popular.collect(&:id)
649
+ # f.input :authors, :value_method => :login, :selected => nil # override any defaults: select none
650
+ #
651
+ # You can pass html_options to the select tag using :input_html => {}
652
+ #
653
+ # Examples:
654
+ #
655
+ # f.input :authors, :input_html => {:size => 20, :multiple => true}
656
+ #
657
+ # By default, all select inputs will have a blank option at the top of the list. You can add
658
+ # a prompt with the :prompt option, or disable the blank option with :include_blank => false.
659
+ #
660
+ #
661
+ # You can group the options in optgroup elements by passing the :group_by option
662
+ # (Note: only tested for belongs_to relations)
663
+ #
664
+ # Examples:
665
+ #
666
+ # f.input :author, :group_by => :continent
667
+ #
668
+ # All the other options should work as expected. If you want to call a custom method on the
669
+ # group item. You can include the option:group_label_method
670
+ # Examples:
671
+ #
672
+ # f.input :author, :group_by => :continents, :group_label_method => :something_different
673
+ #
674
+ def select_input(method, options)
675
+ html_options = options.delete(:input_html) || {}
676
+ options = set_include_blank(options)
677
+ html_options[:multiple] = html_options[:multiple] || options.delete(:multiple)
678
+ html_options.delete(:multiple) if html_options[:multiple].nil?
679
+
680
+ reflection = self.reflection_for(method)
681
+ if reflection && [ :has_many, :has_and_belongs_to_many ].include?(reflection.macro)
682
+ options[:include_blank] = false
683
+ html_options[:multiple] = true if html_options[:multiple].nil?
684
+ html_options[:size] ||= 5
685
+ end
686
+ options[:selected] = options[:selected].first if options[:selected].present? && html_options[:multiple] == false
687
+ input_name = generate_association_input_name(method)
688
+
689
+ select_html = if options[:group_by]
690
+ # The grouped_options_select is a bit counter intuitive and not optimised (mostly due to ActiveRecord).
691
+ # The formtastic user however shouldn't notice this too much.
692
+ raw_collection = find_raw_collection_for_column(method, options.reverse_merge(:find_options => { :include => options[:group_by] }))
693
+ label, value = detect_label_and_value_method!(raw_collection)
694
+ group_collection = raw_collection.map { |option| option.send(options[:group_by]) }.uniq
695
+ group_label_method = options[:group_label_method] || detect_label_method(group_collection)
696
+ group_collection = group_collection.sort_by { |group_item| group_item.send(group_label_method) }
697
+ group_association = options[:group_association] || detect_group_association(method, options[:group_by])
698
+
699
+ # Here comes the monster with 8 arguments
700
+ self.grouped_collection_select(input_name, group_collection,
701
+ group_association, group_label_method,
702
+ value, label,
703
+ strip_formtastic_options(options), html_options)
704
+ else
705
+ collection = find_collection_for_column(method, options)
706
+
707
+ self.select(input_name, collection, strip_formtastic_options(options), html_options)
708
+ end
709
+
710
+ self.label(method, options_for_label(options).merge(:input_name => input_name)) << select_html
711
+ end
712
+ alias :boolean_select_input :select_input
713
+
714
+ # Outputs a timezone select input as Rails' time_zone_select helper. You
715
+ # can give priority zones as option.
716
+ #
717
+ # Examples:
718
+ #
719
+ # f.input :time_zone, :as => :time_zone, :priority_zones => /Australia/
720
+ #
721
+ # You can pre-select a specific option value by passing in the :selected option.
722
+ # Note: Right now only works if the form object attribute value is not set (nil),
723
+ # because of how the core helper is implemented.
724
+ #
725
+ # Examples:
726
+ #
727
+ # f.input :my_favorite_time_zone, :as => :time_zone, :selected => 'Singapore'
728
+ #
729
+ def time_zone_input(method, options)
730
+ html_options = options.delete(:input_html) || {}
731
+ selected_value = options.delete(:selected)
732
+
733
+ self.label(method, options_for_label(options)) <<
734
+ self.time_zone_select(method, options.delete(:priority_zones),
735
+ strip_formtastic_options(options).merge(:default => selected_value), html_options)
736
+ end
737
+
738
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
739
+ # items, one for each possible choice in the belongs_to association. Each li contains a
740
+ # label and a radio input.
741
+ #
742
+ # Example:
743
+ #
744
+ # f.input :author, :as => :radio
745
+ #
746
+ # Output:
747
+ #
748
+ # <fieldset>
749
+ # <legend><span>Author</span></legend>
750
+ # <ol>
751
+ # <li>
752
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id]" type="radio" value="1" /> Justin French</label>
753
+ # </li>
754
+ # <li>
755
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id]" type="radio" value="2" /> Kate French</label>
756
+ # </li>
757
+ # </ol>
758
+ # </fieldset>
759
+ #
760
+ # You can customize the choices available in the radio button set by passing in a collection (an Array or
761
+ # Hash) through the :collection option. If not provided, the choices are found by reflecting on the association
762
+ # (Author.find(:all) in the example above).
763
+ #
764
+ # Examples:
765
+ #
766
+ # f.input :author, :as => :radio, :collection => @authors
767
+ # f.input :author, :as => :radio, :collection => Author.find(:all)
768
+ # f.input :author, :as => :radio, :collection => [@justin, @kate]
769
+ # f.input :author, :collection => ["Justin", "Kate", "Amelia", "Gus", "Meg"]
770
+ #
771
+ # The :label_method option allows you to customize the label for each radio button two ways:
772
+ #
773
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
774
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
775
+ #
776
+ # Examples:
777
+ #
778
+ # f.input :author, :as => :radio, :label_method => :full_name
779
+ # f.input :author, :as => :radio, :label_method => :login
780
+ # f.input :author, :as => :radio, :label_method => :full_name_with_post_count
781
+ # f.input :author, :as => :radio, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
782
+ #
783
+ # The :value_method option provides the same customization of the value attribute of each option tag.
784
+ #
785
+ # Examples:
786
+ #
787
+ # f.input :author, :as => :radio, :value_method => :full_name
788
+ # f.input :author, :as => :radio, :value_method => :login
789
+ # f.input :author, :as => :radio, :value_method => Proc.new { |a| "author_#{a.login}" }
790
+ #
791
+ # You can force a particular radio button in the collection to be checked with the :selected option.
792
+ #
793
+ # Examples:
794
+ #
795
+ # f.input :subscribe_to_newsletter, :as => :radio, :selected => true
796
+ # f.input :subscribe_to_newsletter, :as => :radio, :collection => ["Yeah!", "Nope!"], :selected => "Nope!"
797
+ #
798
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each radio
799
+ # button / label combination to contain a class with the value of the radio button (useful for
800
+ # applying specific CSS or Javascript to a particular radio button).
801
+ #
802
+ def radio_input(method, options)
803
+ collection = find_collection_for_column(method, options)
804
+ html_options = strip_formtastic_options(options).merge(options.delete(:input_html) || {})
805
+
806
+ input_name = generate_association_input_name(method)
807
+ value_as_class = options.delete(:value_as_class)
808
+ input_ids = []
809
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
810
+ selected_value = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
811
+
812
+ list_item_content = collection.map do |c|
813
+ label = c.is_a?(Array) ? c.first : c
814
+ value = c.is_a?(Array) ? c.last : c
815
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
816
+ input_ids << input_id
817
+
818
+ html_options[:checked] = selected_value == value if selected_option_is_present
819
+
820
+ li_content = template.content_tag(:label,
821
+ "#{self.radio_button(input_name, value, html_options)} #{label}",
822
+ :for => input_id
823
+ )
824
+
825
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
826
+ template.content_tag(:li, li_content, li_options)
827
+ end
828
+
829
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
830
+ end
831
+ alias :boolean_radio_input :radio_input
832
+
833
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
834
+ # items (li), one for each fragment for the date (year, month, day). Each li contains a label
835
+ # (eg "Year") and a select box. See date_or_datetime_input for a more detailed output example.
836
+ #
837
+ # You can pre-select a specific option value by passing in the :selected option.
838
+ #
839
+ # Examples:
840
+ #
841
+ # f.input :created_at, :as => :date, :selected => 1.day.ago
842
+ # f.input :created_at, :as => :date, :selected => nil # override any defaults: select none
843
+ #
844
+ # Some of Rails' options for select_date are supported, but not everything yet.
845
+ #
846
+ def date_input(method, options)
847
+ options = set_include_blank(options)
848
+ date_or_datetime_input(method, options.merge(:discard_hour => true))
849
+ end
850
+
851
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
852
+ # items (li), one for each fragment for the date (year, month, day, hour, min, sec). Each li
853
+ # contains a label (eg "Year") and a select box. See date_or_datetime_input for a more
854
+ # detailed output example.
855
+ #
856
+ # You can pre-select a specific option value by passing in the :selected option.
857
+ #
858
+ # Examples:
859
+ #
860
+ # f.input :created_at, :as => :datetime, :selected => 1.day.ago
861
+ # f.input :created_at, :as => :datetime, :selected => nil # override any defaults: select none
862
+ #
863
+ # Some of Rails' options for select_date are supported, but not everything yet.
864
+ #
865
+ def datetime_input(method, options)
866
+ options = set_include_blank(options)
867
+ date_or_datetime_input(method, options)
868
+ end
869
+
870
+ # Outputs a fieldset with a legend for the method label, and a ordered list (ol) of list
871
+ # items (li), one for each fragment for the time (hour, minute, second). Each li contains a label
872
+ # (eg "Hour") and a select box. See date_or_datetime_input for a more detailed output example.
873
+ #
874
+ # You can pre-select a specific option value by passing in the :selected option.
875
+ #
876
+ # Examples:
877
+ #
878
+ # f.input :created_at, :as => :time, :selected => 1.hour.ago
879
+ # f.input :created_at, :as => :time, :selected => nil # override any defaults: select none
880
+ #
881
+ # Some of Rails' options for select_time are supported, but not everything yet.
882
+ #
883
+ def time_input(method, options)
884
+ options = set_include_blank(options)
885
+ date_or_datetime_input(method, options.merge(:discard_year => true, :discard_month => true, :discard_day => true))
886
+ end
887
+
888
+ # <fieldset>
889
+ # <legend>Created At</legend>
890
+ # <ol>
891
+ # <li>
892
+ # <label for="user_created_at_1i">Year</label>
893
+ # <select id="user_created_at_1i" name="user[created_at(1i)]">
894
+ # <option value="2003">2003</option>
895
+ # ...
896
+ # <option value="2013">2013</option>
897
+ # </select>
898
+ # </li>
899
+ # <li>
900
+ # <label for="user_created_at_2i">Month</label>
901
+ # <select id="user_created_at_2i" name="user[created_at(2i)]">
902
+ # <option value="1">January</option>
903
+ # ...
904
+ # <option value="12">December</option>
905
+ # </select>
906
+ # </li>
907
+ # <li>
908
+ # <label for="user_created_at_3i">Day</label>
909
+ # <select id="user_created_at_3i" name="user[created_at(3i)]">
910
+ # <option value="1">1</option>
911
+ # ...
912
+ # <option value="31">31</option>
913
+ # </select>
914
+ # </li>
915
+ # </ol>
916
+ # </fieldset>
917
+ #
918
+ # This is an absolute abomination, but so is the official Rails select_date().
919
+ #
920
+ def date_or_datetime_input(method, options)
921
+ position = { :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6 }
922
+ i18n_date_order = ::I18n.t(:order, :scope => [:date])
923
+ i18n_date_order = nil unless i18n_date_order.is_a?(Array)
924
+ inputs = options.delete(:order) || i18n_date_order || [:year, :month, :day]
925
+
926
+ time_inputs = [:hour, :minute]
927
+ time_inputs << [:second] if options[:include_seconds]
928
+
929
+ list_items_capture = ""
930
+ hidden_fields_capture = ""
931
+
932
+ default_time = ::Time.now
933
+
934
+ # Gets the datetime object. It can be a Fixnum, Date or Time, or nil.
935
+ datetime = options[:selected] || (@object ? @object.send(method) : default_time) || default_time
936
+
937
+ html_options = options.delete(:input_html) || {}
938
+ input_ids = []
939
+
940
+ (inputs + time_inputs).each do |input|
941
+ input_ids << input_id = generate_html_id(method, "#{position[input]}i")
942
+
943
+ field_name = "#{method}(#{position[input]}i)"
944
+ if options[:"discard_#{input}"]
945
+ break if time_inputs.include?(input)
946
+
947
+ hidden_value = datetime.respond_to?(input) ? datetime.send(input.to_sym) : datetime
948
+ hidden_fields_capture << template.hidden_field_tag("#{@object_name}[#{field_name}]", (hidden_value || 1), :id => input_id)
949
+ else
950
+ opts = strip_formtastic_options(options).merge(:prefix => @object_name, :field_name => field_name, :default => datetime)
951
+ item_label_text = ::I18n.t(input.to_s, :default => input.to_s.humanize, :scope => [:datetime, :prompts])
952
+
953
+ list_items_capture << template.content_tag(:li,
954
+ template.content_tag(:label, item_label_text, :for => input_id) <<
955
+ template.send(:"select_#{input}", datetime, opts, html_options.merge(:id => input_id))
956
+ )
957
+ end
958
+ end
959
+
960
+ hidden_fields_capture << field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_items_capture)
961
+ end
962
+
963
+ # Outputs a fieldset containing a legend for the label text, and an ordered list (ol) of list
964
+ # items, one for each possible choice in the belongs_to association. Each li contains a
965
+ # label and a check_box input.
966
+ #
967
+ # This is an alternative for has many and has and belongs to many associations.
968
+ #
969
+ # Example:
970
+ #
971
+ # f.input :author, :as => :check_boxes
972
+ #
973
+ # Output:
974
+ #
975
+ # <fieldset>
976
+ # <legend class="label"><label>Authors</label></legend>
977
+ # <ol>
978
+ # <li>
979
+ # <input type="hidden" name="book[author_id][1]" value="">
980
+ # <label for="book_author_id_1"><input id="book_author_id_1" name="book[author_id][1]" type="checkbox" value="1" /> Justin French</label>
981
+ # </li>
982
+ # <li>
983
+ # <input type="hidden" name="book[author_id][2]" value="">
984
+ # <label for="book_author_id_2"><input id="book_author_id_2" name="book[owner_id][2]" type="checkbox" value="2" /> Kate French</label>
985
+ # </li>
986
+ # </ol>
987
+ # </fieldset>
988
+ #
989
+ # Notice that the value of the checkbox is the same as the id and the hidden
990
+ # field has empty value. You can override the hidden field value using the
991
+ # unchecked_value option.
992
+ #
993
+ # You can customize the options available in the set by passing in a collection (Array) of
994
+ # ActiveRecord objects through the :collection option. If not provided, the choices are found
995
+ # by inferring the parent's class name from the method name and simply calling find(:all) on
996
+ # it (Author.find(:all) in the example above).
997
+ #
998
+ # Examples:
999
+ #
1000
+ # f.input :author, :as => :check_boxes, :collection => @authors
1001
+ # f.input :author, :as => :check_boxes, :collection => Author.find(:all)
1002
+ # f.input :author, :as => :check_boxes, :collection => [@justin, @kate]
1003
+ #
1004
+ # The :label_method option allows you to customize the label for each checkbox two ways:
1005
+ #
1006
+ # * by naming the correct method to call on each object in the collection as a symbol (:name, :login, etc)
1007
+ # * by passing a Proc that will be called on each object in the collection, allowing you to use helpers or multiple model attributes together
1008
+ #
1009
+ # Examples:
1010
+ #
1011
+ # f.input :author, :as => :check_boxes, :label_method => :full_name
1012
+ # f.input :author, :as => :check_boxes, :label_method => :login
1013
+ # f.input :author, :as => :check_boxes, :label_method => :full_name_with_post_count
1014
+ # f.input :author, :as => :check_boxes, :label_method => Proc.new { |a| "#{a.name} (#{pluralize("post", a.posts.count)})" }
1015
+ #
1016
+ # The :value_method option provides the same customization of the value attribute of each checkbox input tag.
1017
+ #
1018
+ # Examples:
1019
+ #
1020
+ # f.input :author, :as => :check_boxes, :value_method => :full_name
1021
+ # f.input :author, :as => :check_boxes, :value_method => :login
1022
+ # f.input :author, :as => :check_boxes, :value_method => Proc.new { |a| "author_#{a.login}" }
1023
+ #
1024
+ # You can pre-select/check a specific checkbox value by passing in the :selected option (alias :checked works as well).
1025
+ #
1026
+ # Examples:
1027
+ #
1028
+ # f.input :authors, :as => :check_boxes, :selected => @justin
1029
+ # f.input :authors, :as => :check_boxes, :selected => Author.most_popular.collect(&:id)
1030
+ # f.input :authors, :as => :check_boxes, :selected => nil # override any defaults: select none
1031
+ #
1032
+ # Finally, you can set :value_as_class => true if you want the li wrapper around each checkbox / label
1033
+ # combination to contain a class with the value of the radio button (useful for applying specific
1034
+ # CSS or Javascript to a particular checkbox).
1035
+ #
1036
+ def check_boxes_input(method, options)
1037
+ collection = find_collection_for_column(method, options)
1038
+ html_options = options.delete(:input_html) || {}
1039
+
1040
+ input_name = generate_association_input_name(method)
1041
+ value_as_class = options.delete(:value_as_class)
1042
+ unchecked_value = options.delete(:unchecked_value) || ''
1043
+ html_options = { :name => "#{@object_name}[#{input_name}][]" }.merge(html_options)
1044
+ input_ids = []
1045
+
1046
+ selected_option_is_present = [:selected, :checked].any? { |k| options.key?(k) }
1047
+ selected_values = (options.key?(:checked) ? options[:checked] : options[:selected]) if selected_option_is_present
1048
+ selected_values = [*selected_values].compact
1049
+
1050
+ list_item_content = collection.map do |c|
1051
+ label = c.is_a?(Array) ? c.first : c
1052
+ value = c.is_a?(Array) ? c.last : c
1053
+ input_id = generate_html_id(input_name, value.to_s.gsub(/\s/, '_').gsub(/\W/, '').downcase)
1054
+ input_ids << input_id
1055
+
1056
+ html_options[:checked] = selected_values.include?(value) if selected_option_is_present
1057
+ html_options[:id] = input_id
1058
+
1059
+ li_content = template.content_tag(:label,
1060
+ "#{self.check_box(input_name, html_options, value, unchecked_value)} #{label}",
1061
+ :for => input_id
1062
+ )
1063
+
1064
+ li_options = value_as_class ? { :class => [method.to_s.singularize, value.to_s.downcase].join('_') } : {}
1065
+ template.content_tag(:li, li_content, li_options)
1066
+ end
1067
+
1068
+ field_set_and_list_wrapping_for_method(method, options.merge(:label_for => input_ids.first), list_item_content)
1069
+ end
1070
+
1071
+ # Outputs a country select input, wrapping around a regular country_select helper.
1072
+ # Rails doesn't come with a country_select helper by default any more, so you'll need to install
1073
+ # the "official" plugin, or, if you wish, any other country_select plugin that behaves in the
1074
+ # same way.
1075
+ #
1076
+ # The Rails plugin iso-3166-country-select plugin can be found "here":http://github.com/rails/iso-3166-country-select.
1077
+ #
1078
+ # By default, Formtastic includes a handfull of english-speaking countries as "priority counties",
1079
+ # which you can change to suit your market and user base (see README for more info on config).
1080
+ #
1081
+ # Examples:
1082
+ # f.input :location, :as => :country # use Formtastic::SemanticFormBuilder.priority_countries array for the priority countries
1083
+ # f.input :location, :as => :country, :priority_countries => /Australia/ # set your own
1084
+ #
1085
+ def country_input(method, options)
1086
+ raise "To use the :country input, please install a country_select plugin, like this one: http://github.com/rails/iso-3166-country-select" unless self.respond_to?(:country_select)
1087
+
1088
+ html_options = options.delete(:input_html) || {}
1089
+ priority_countries = options.delete(:priority_countries) || @@priority_countries
1090
+
1091
+ self.label(method, options_for_label(options)) <<
1092
+ self.country_select(method, priority_countries, strip_formtastic_options(options), html_options)
1093
+ end
1094
+
1095
+ # Outputs a label containing a checkbox and the label text. The label defaults
1096
+ # to the column name (method name) and can be altered with the :label option.
1097
+ # :checked_value and :unchecked_value options are also available.
1098
+ #
1099
+ # You can pre-select/check the boolean checkbox by passing in the :selected option (alias :checked works as well).
1100
+ #
1101
+ # Examples:
1102
+ #
1103
+ # f.input :allow_comments, :as => :boolean, :selected => true # override any default value: selected/checked
1104
+ #
1105
+ def boolean_input(method, options)
1106
+ html_options = options.delete(:input_html) || {}
1107
+ checked = options.key?(:checked) ? options[:checked] : options[:selected]
1108
+ html_options[:checked] = checked == true if [:selected, :checked].any? { |k| options.key?(k) }
1109
+
1110
+ input = self.check_box(method, strip_formtastic_options(options).merge(html_options),
1111
+ options.delete(:checked_value) || '1', options.delete(:unchecked_value) || '0')
1112
+ options = options_for_label(options)
1113
+
1114
+ # the label() method will insert this nested input into the label at the last minute
1115
+ options[:label_prefix_for_nested_input] = input
1116
+
1117
+ self.label(method, options)
1118
+ end
1119
+
1120
+ # Generates an input for the given method using the type supplied with :as.
1121
+ def inline_input_for(method, options)
1122
+ send(:"#{options.delete(:as)}_input", method, options)
1123
+ end
1124
+
1125
+ # Generates hints for the given method using the text supplied in :hint.
1126
+ #
1127
+ def inline_hints_for(method, options) #:nodoc:
1128
+ options[:hint] = localized_string(method, options[:hint], :hint)
1129
+ return if options[:hint].blank?
1130
+ template.content_tag(:p, options[:hint], :class => 'inline-hints')
1131
+ end
1132
+
1133
+ # Creates an error sentence by calling to_sentence on the errors array.
1134
+ #
1135
+ def error_sentence(errors) #:nodoc:
1136
+ template.content_tag(:p, errors.to_sentence.untaint, :class => 'inline-errors')
1137
+ end
1138
+
1139
+ # Creates an error li list.
1140
+ #
1141
+ def error_list(errors) #:nodoc:
1142
+ list_elements = []
1143
+ errors.each do |error|
1144
+ list_elements << template.content_tag(:li, error.untaint)
1145
+ end
1146
+ template.content_tag(:ul, list_elements.join("\n"), :class => 'errors')
1147
+ end
1148
+
1149
+ # Creates an error sentence containing only the first error
1150
+ #
1151
+ def error_first(errors) #:nodoc:
1152
+ template.content_tag(:p, errors.first.untaint, :class => 'inline-errors')
1153
+ end
1154
+
1155
+ # Generates the required or optional string. If the value set is a proc,
1156
+ # it evaluates the proc first.
1157
+ #
1158
+ def required_or_optional_string(required) #:nodoc:
1159
+ string_or_proc = case required
1160
+ when true
1161
+ @@required_string
1162
+ when false
1163
+ @@optional_string
1164
+ else
1165
+ required
1166
+ end
1167
+
1168
+ if string_or_proc.is_a?(Proc)
1169
+ string_or_proc.call
1170
+ else
1171
+ string_or_proc.to_s
1172
+ end
1173
+ end
1174
+
1175
+ # Generates a fieldset and wraps the content in an ordered list. When working
1176
+ # with nested attributes (in Rails 2.3), it allows %i as interpolation option
1177
+ # in :name. So you can do:
1178
+ #
1179
+ # f.inputs :name => 'Task #%i', :for => :tasks
1180
+ #
1181
+ # or the shorter equivalent:
1182
+ #
1183
+ # f.inputs 'Task #%i', :for => :tasks
1184
+ #
1185
+ # And it will generate a fieldset for each task with legend 'Task #1', 'Task #2',
1186
+ # 'Task #3' and so on.
1187
+ #
1188
+ # Note: Special case for the inline inputs (non-block):
1189
+ # f.inputs "My little legend", :title, :body, :author # Explicit legend string => "My little legend"
1190
+ # f.inputs :my_little_legend, :title, :body, :author # Localized (118n) legend with I18n key => I18n.t(:my_little_legend, ...)
1191
+ # f.inputs :title, :body, :author # First argument is a column => (no legend)
1192
+ #
1193
+ def field_set_and_list_wrapping(*args, &block) #:nodoc:
1194
+ contents = args.last.is_a?(::Hash) ? '' : args.pop.flatten
1195
+ html_options = args.extract_options!
1196
+
1197
+ legend = html_options.delete(:name).to_s
1198
+ legend %= parent_child_index(html_options[:parent]) if html_options[:parent]
1199
+ legend = template.content_tag(:legend, template.content_tag(:span, legend)) unless legend.blank?
1200
+
1201
+ if block_given?
1202
+ contents = if template.respond_to?(:is_haml?) && template.is_haml?
1203
+ template.capture_haml(&block)
1204
+ else
1205
+ template.capture(&block)
1206
+ end
1207
+ end
1208
+
1209
+ # Ruby 1.9: String#to_s behavior changed, need to make an explicit join.
1210
+ contents = contents.join if contents.respond_to?(:join)
1211
+ fieldset = template.content_tag(:fieldset,
1212
+ legend << template.content_tag(:ol, contents),
1213
+ html_options.except(:builder, :parent)
1214
+ )
1215
+
1216
+ template.concat(fieldset) if block_given?
1217
+ fieldset
1218
+ end
1219
+
1220
+ def field_set_title_from_args(*args) #:nodoc:
1221
+ options = args.extract_options!
1222
+ options[:name] ||= options.delete(:title)
1223
+ title = options[:name]
1224
+
1225
+ if title.blank?
1226
+ valid_name_classes = [::String, ::Symbol]
1227
+ valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && self.content_columns.include?(args.first))
1228
+ title = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
1229
+ end
1230
+ title = localized_string(title, title, :title) if title.is_a?(::Symbol)
1231
+ title
1232
+ end
1233
+
1234
+ # Also generates a fieldset and an ordered list but with label based in
1235
+ # method. This methods is currently used by radio and datetime inputs.
1236
+ #
1237
+ def field_set_and_list_wrapping_for_method(method, options, contents) #:nodoc:
1238
+ contents = contents.join if contents.respond_to?(:join)
1239
+
1240
+ template.content_tag(:fieldset,
1241
+ template.content_tag(:legend,
1242
+ self.label(method, options_for_label(options).merge(:for => options.delete(:label_for))), :class => 'label'
1243
+ ) <<
1244
+ template.content_tag(:ol, contents)
1245
+ )
1246
+ end
1247
+
1248
+ # For methods that have a database column, take a best guess as to what the input method
1249
+ # should be. In most cases, it will just return the column type (eg :string), but for special
1250
+ # cases it will simplify (like the case of :integer, :float & :decimal to :numeric), or do
1251
+ # something different (like :password and :select).
1252
+ #
1253
+ # If there is no column for the method (eg "virtual columns" with an attr_accessor), the
1254
+ # default is a :string, a similar behaviour to Rails' scaffolding.
1255
+ #
1256
+ def default_input_type(method, options = {}) #:nodoc:
1257
+ if column = self.column_for(method)
1258
+ # Special cases where the column type doesn't map to an input method.
1259
+ case column.type
1260
+ when :string
1261
+ return :password if method.to_s =~ /password/
1262
+ return :country if method.to_s =~ /country/
1263
+ return :time_zone if method.to_s =~ /time_zone/
1264
+ when :integer
1265
+ return :select if method.to_s =~ /_id$/
1266
+ return :numeric
1267
+ when :float, :decimal
1268
+ return :numeric
1269
+ when :timestamp
1270
+ return :datetime
1271
+ end
1272
+
1273
+ # Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
1274
+ return :select if column.type == :string && options.key?(:collection)
1275
+ # Try 3: Assume the input name will be the same as the column type (e.g. string_input).
1276
+ return column.type
1277
+ else
1278
+ if @object
1279
+ return :select if self.reflection_for(method)
1280
+
1281
+ file = @object.send(method) if @object.respond_to?(method)
1282
+ return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
1283
+ end
1284
+
1285
+ return :select if options.key?(:collection)
1286
+ return :password if method.to_s =~ /password/
1287
+ return :string
1288
+ end
1289
+ end
1290
+
1291
+ # Used by select and radio inputs. The collection can be retrieved by
1292
+ # three ways:
1293
+ #
1294
+ # * Explicitly provided through :collection
1295
+ # * Retrivied through an association
1296
+ # * Or a boolean column, which will generate a localized { "Yes" => true, "No" => false } hash.
1297
+ #
1298
+ # If the collection is not a hash or an array of strings, fixnums or arrays,
1299
+ # we use label_method and value_method to retreive an array with the
1300
+ # appropriate label and value.
1301
+ #
1302
+ def find_collection_for_column(column, options) #:nodoc:
1303
+ collection = find_raw_collection_for_column(column, options)
1304
+
1305
+ # Return if we have an Array of strings, fixnums or arrays
1306
+ return collection if (collection.instance_of?(Array) || collection.instance_of?(Range)) &&
1307
+ [Array, Fixnum, String, Symbol].include?(collection.first.class)
1308
+
1309
+ label, value = detect_label_and_value_method!(collection, options)
1310
+ collection.map { |o| [send_or_call(label, o), send_or_call(value, o)] }
1311
+ end
1312
+
1313
+ # As #find_collection_for_column but returns the collection without mapping the label and value
1314
+ #
1315
+ def find_raw_collection_for_column(column, options) #:nodoc:
1316
+ collection = if options[:collection]
1317
+ options.delete(:collection)
1318
+ elsif reflection = self.reflection_for(column)
1319
+ reflection.klass.find(:all, options[:find_options] || {})
1320
+ else
1321
+ create_boolean_collection(options)
1322
+ end
1323
+
1324
+ collection = collection.to_a if collection.is_a?(Hash)
1325
+ collection
1326
+ end
1327
+
1328
+ # Detects the label and value methods from a collection values set in
1329
+ # @@collection_label_methods. It will use and delete
1330
+ # the options :label_method and :value_methods when present
1331
+ #
1332
+ def detect_label_and_value_method!(collection_or_instance, options = {}) #:nodoc
1333
+ label = options.delete(:label_method) || detect_label_method(collection_or_instance)
1334
+ value = options.delete(:value_method) || :id
1335
+ [label, value]
1336
+ end
1337
+
1338
+ # Detected the label collection method when none is supplied using the
1339
+ # values set in @@collection_label_methods.
1340
+ #
1341
+ def detect_label_method(collection) #:nodoc:
1342
+ @@collection_label_methods.detect { |m| collection.first.respond_to?(m) }
1343
+ end
1344
+
1345
+ # Detects the method to call for fetching group members from the groups when grouping select options
1346
+ #
1347
+ def detect_group_association(method, group_by)
1348
+ object_to_method_reflection = self.reflection_for(method)
1349
+ method_class = object_to_method_reflection.klass
1350
+
1351
+ method_to_group_association = method_class.reflect_on_association(group_by)
1352
+ group_class = method_to_group_association.klass
1353
+
1354
+ # This will return in the normal case
1355
+ return method.to_s.pluralize.to_sym if group_class.reflect_on_association(method.to_s.pluralize)
1356
+
1357
+ # This is for belongs_to associations named differently than their class
1358
+ # form.input :parent, :group_by => :customer
1359
+ # eg.
1360
+ # class Project
1361
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1362
+ # belongs_to :customer
1363
+ # end
1364
+ # class Customer
1365
+ # has_many :projects
1366
+ # end
1367
+ group_method = method_class.to_s.underscore.pluralize.to_sym
1368
+ return group_method if group_class.reflect_on_association(group_method) # :projects
1369
+
1370
+ # This is for has_many associations named differently than their class
1371
+ # eg.
1372
+ # class Project
1373
+ # belongs_to :parent, :class_name => 'Project', :foreign_key => 'parent_id'
1374
+ # belongs_to :customer
1375
+ # end
1376
+ # class Customer
1377
+ # has_many :tasks, :class_name => 'Project', :foreign_key => 'customer_id'
1378
+ # end
1379
+ possible_associations = group_class.reflect_on_all_associations(:has_many).find_all{|assoc| assoc.klass == object_class}
1380
+ return possible_associations.first.name.to_sym if possible_associations.count == 1
1381
+
1382
+ raise "Cannot infer group association for #{method} grouped by #{group_by}, there were #{possible_associations.empty? ? 'no' : possible_associations.size} possible associations. Please specify using :group_association"
1383
+
1384
+ end
1385
+
1386
+ # Returns a hash to be used by radio and select inputs when a boolean field
1387
+ # is provided.
1388
+ #
1389
+ def create_boolean_collection(options) #:nodoc:
1390
+ options[:true] ||= ::Formtastic::I18n.t(:yes)
1391
+ options[:false] ||= ::Formtastic::I18n.t(:no)
1392
+ options[:value_as_class] = true unless options.key?(:value_as_class)
1393
+
1394
+ [ [ options.delete(:true), true], [ options.delete(:false), false ] ]
1395
+ end
1396
+
1397
+ # Used by association inputs (select, radio) to generate the name that should
1398
+ # be used for the input
1399
+ #
1400
+ # belongs_to :author; f.input :author; will generate 'author_id'
1401
+ # belongs_to :entity, :foreign_key = :owner_id; f.input :author; will generate 'owner_id'
1402
+ # has_many :authors; f.input :authors; will generate 'author_ids'
1403
+ # has_and_belongs_to_many will act like has_many
1404
+ #
1405
+ def generate_association_input_name(method) #:nodoc:
1406
+ if reflection = self.reflection_for(method)
1407
+ if [:has_and_belongs_to_many, :has_many].include?(reflection.macro)
1408
+ "#{method.to_s.singularize}_ids"
1409
+ else
1410
+ reflection.options[:foreign_key] || "#{method}_id"
1411
+ end
1412
+ else
1413
+ method
1414
+ end.to_sym
1415
+ end
1416
+
1417
+ # If an association method is passed in (f.input :author) try to find the
1418
+ # reflection object.
1419
+ #
1420
+ def reflection_for(method) #:nodoc:
1421
+ @object.class.reflect_on_association(method) if @object.class.respond_to?(:reflect_on_association)
1422
+ end
1423
+
1424
+ # Get a column object for a specified attribute method - if possible.
1425
+ #
1426
+ def column_for(method) #:nodoc:
1427
+ @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
1428
+ end
1429
+
1430
+ # Generates default_string_options by retrieving column information from
1431
+ # the database.
1432
+ #
1433
+ def default_string_options(method, type) #:nodoc:
1434
+ column = self.column_for(method)
1435
+
1436
+ if type == :text
1437
+ { :cols => @@default_text_field_size, :rows => @@default_text_area_height }
1438
+ elsif type == :numeric || column.nil? || column.limit.nil?
1439
+ { :size => @@default_text_field_size }
1440
+ else
1441
+ { :maxlength => column.limit, :size => [column.limit, @@default_text_field_size].min }
1442
+ end
1443
+ end
1444
+
1445
+ # Generate the html id for the li tag.
1446
+ # It takes into account options[:index] and @auto_index to generate li
1447
+ # elements with appropriate index scope. It also sanitizes the object
1448
+ # and method names.
1449
+ #
1450
+ def generate_html_id(method_name, value='input') #:nodoc:
1451
+ if options.has_key?(:index)
1452
+ index = "_#{options[:index]}"
1453
+ elsif defined?(@auto_index)
1454
+ index = "_#{@auto_index}"
1455
+ else
1456
+ index = ""
1457
+ end
1458
+ sanitized_method_name = method_name.to_s.gsub(/[\?\/\-]$/, '')
1459
+
1460
+ "#{sanitized_object_name}#{index}_#{sanitized_method_name}_#{value}"
1461
+ end
1462
+
1463
+ # Gets the nested_child_index value from the parent builder. In Rails 2.3
1464
+ # it always returns a fixnum. In next versions it returns a hash with each
1465
+ # association that the parent builds.
1466
+ #
1467
+ def parent_child_index(parent) #:nodoc:
1468
+ duck = parent[:builder].instance_variable_get('@nested_child_index')
1469
+
1470
+ if duck.is_a?(Hash)
1471
+ child = parent[:for]
1472
+ child = child.first if child.respond_to?(:first)
1473
+ duck[child].to_i + 1
1474
+ else
1475
+ duck.to_i + 1
1476
+ end
1477
+ end
1478
+
1479
+ def sanitized_object_name #:nodoc:
1480
+ @sanitized_object_name ||= @object_name.to_s.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
1481
+ end
1482
+
1483
+ def humanized_attribute_name(method) #:nodoc:
1484
+ if @object && @object.class.respond_to?(:human_attribute_name) && @@label_str_method == :humanize
1485
+ @object.class.human_attribute_name(method.to_s)
1486
+ else
1487
+ method.to_s.send(@@label_str_method)
1488
+ end
1489
+ end
1490
+
1491
+ # Internal generic method for looking up localized values within Formtastic
1492
+ # using I18n, if no explicit value is set and I18n-lookups are enabled.
1493
+ #
1494
+ # Enabled/Disable this by setting:
1495
+ #
1496
+ # Formtastic::SemanticFormBuilder.i18n_lookups_by_default = true/false
1497
+ #
1498
+ # Lookup priority:
1499
+ #
1500
+ # 'formtastic.{{type}}.{{model}}.{{action}}.{{attribute}}'
1501
+ # 'formtastic.{{type}}.{{model}}.{{attribute}}'
1502
+ # 'formtastic.{{type}}.{{attribute}}'
1503
+ #
1504
+ # Example:
1505
+ #
1506
+ # 'formtastic.labels.post.edit.title'
1507
+ # 'formtastic.labels.post.title'
1508
+ # 'formtastic.labels.title'
1509
+ #
1510
+ # NOTE: Generic, but only used for form input titles/labels/hints/actions (titles = legends, actions = buttons).
1511
+ #
1512
+ def localized_string(key, value, type, options = {}) #:nodoc:
1513
+ key = value if value.is_a?(::Symbol)
1514
+
1515
+ if value.is_a?(::String)
1516
+ value
1517
+ else
1518
+ use_i18n = value.nil? ? @@i18n_lookups_by_default : (value != false)
1519
+
1520
+ if use_i18n
1521
+ model_name = self.model_name.underscore
1522
+ action_name = template.params[:action].to_s rescue ''
1523
+ attribute_name = key.to_s
1524
+
1525
+ defaults = ::Formtastic::I18n::SCOPES.collect do |i18n_scope|
1526
+ i18n_path = i18n_scope.dup
1527
+ i18n_path.gsub!('{{action}}', action_name)
1528
+ i18n_path.gsub!('{{model}}', model_name)
1529
+ i18n_path.gsub!('{{attribute}}', attribute_name)
1530
+ i18n_path.gsub!('..', '.')
1531
+ i18n_path.to_sym
1532
+ end
1533
+ defaults << ''
1534
+
1535
+ i18n_value = ::Formtastic::I18n.t(defaults.shift,
1536
+ options.merge(:default => defaults, :scope => type.to_s.pluralize.to_sym))
1537
+ i18n_value.blank? ? nil : i18n_value
1538
+ end
1539
+ end
1540
+ end
1541
+
1542
+ def model_name
1543
+ @object.present? ? @object.class.name : @object_name.to_s.classify
1544
+ end
1545
+
1546
+ def send_or_call(duck, object)
1547
+ if duck.is_a?(Proc)
1548
+ duck.call(object)
1549
+ else
1550
+ object.send(duck)
1551
+ end
1552
+ end
1553
+
1554
+ def set_include_blank(options)
1555
+ unless options.key?(:include_blank) || options.key?(:prompt)
1556
+ options[:include_blank] = @@include_blank_for_select_by_default
1557
+ end
1558
+ options
1559
+ end
1560
+
1561
+ end
1562
+
1563
+ # Wrappers around form_for (etc) with :builder => SemanticFormBuilder.
1564
+ #
1565
+ # * semantic_form_for(@post)
1566
+ # * semantic_fields_for(@post)
1567
+ # * semantic_form_remote_for(@post)
1568
+ # * semantic_remote_form_for(@post)
1569
+ #
1570
+ # Each of which are the equivalent of:
1571
+ #
1572
+ # * form_for(@post, :builder => Formtastic::SemanticFormBuilder))
1573
+ # * fields_for(@post, :builder => Formtastic::SemanticFormBuilder))
1574
+ # * form_remote_for(@post, :builder => Formtastic::SemanticFormBuilder))
1575
+ # * remote_form_for(@post, :builder => Formtastic::SemanticFormBuilder))
1576
+ #
1577
+ # Example Usage:
1578
+ #
1579
+ # <% semantic_form_for @post do |f| %>
1580
+ # <%= f.input :title %>
1581
+ # <%= f.input :body %>
1582
+ # <% end %>
1583
+ #
1584
+ # The above examples use a resource-oriented style of form_for() helper where only the @post
1585
+ # object is given as an argument, but the generic style is also supported, as are forms with
1586
+ # inline objects (Post.new) rather than objects with instance variables (@post):
1587
+ #
1588
+ # <% semantic_form_for :post, @post, :url => posts_path do |f| %>
1589
+ # ...
1590
+ # <% end %>
1591
+ #
1592
+ # <% semantic_form_for :post, Post.new, :url => posts_path do |f| %>
1593
+ # ...
1594
+ # <% end %>
1595
+ module SemanticFormHelper
1596
+ @@builder = ::Formtastic::SemanticFormBuilder
1597
+ mattr_accessor :builder
1598
+
1599
+ @@default_field_error_proc = nil
1600
+
1601
+ # Override the default ActiveRecordHelper behaviour of wrapping the input.
1602
+ # This gets taken care of semantically by adding an error class to the LI tag
1603
+ # containing the input.
1604
+ #
1605
+ FIELD_ERROR_PROC = proc do |html_tag, instance_tag|
1606
+ html_tag
1607
+ end
1608
+
1609
+ def with_custom_field_error_proc(&block)
1610
+ @@default_field_error_proc = ::ActionView::Base.field_error_proc
1611
+ ::ActionView::Base.field_error_proc = FIELD_ERROR_PROC
1612
+ result = yield
1613
+ ::ActionView::Base.field_error_proc = @@default_field_error_proc
1614
+ result
1615
+ end
1616
+
1617
+ [:form_for, :fields_for, :remote_form_for].each do |meth|
1618
+ src = <<-END_SRC
1619
+ def semantic_#{meth}(record_or_name_or_array, *args, &proc)
1620
+ options = args.extract_options!
1621
+ options[:builder] ||= @@builder
1622
+ options[:html] ||= {}
1623
+
1624
+ class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
1625
+ class_names << "formtastic"
1626
+ class_names << case record_or_name_or_array
1627
+ when String, Symbol then record_or_name_or_array.to_s # :post => "post"
1628
+ when Array then record_or_name_or_array.last.class.to_s.underscore # [@post, @comment] # => "comment"
1629
+ else record_or_name_or_array.class.to_s.underscore # @post => "post"
1630
+ end
1631
+ options[:html][:class] = class_names.join(" ")
1632
+
1633
+ with_custom_field_error_proc do
1634
+ #{meth}(record_or_name_or_array, *(args << options), &proc)
1635
+ end
1636
+ end
1637
+ END_SRC
1638
+ module_eval src, __FILE__, __LINE__
1639
+ end
1640
+ alias :semantic_form_remote_for :semantic_remote_form_for
1641
+
1642
+ end
1643
+ end