formtastic-rails3 0.9.7

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