-
Notifications
You must be signed in to change notification settings - Fork 630
/
inputs_helper.rb
420 lines (403 loc) · 17.2 KB
/
inputs_helper.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
module Formtastic
module Helpers
# {#inputs} is used to wrap a series of form items in a `<fieldset>` and `<ol>`, with each item
# in the list containing the markup representing a single {#input}.
#
# {#inputs} is usually called with a block containing a series of {#input} methods:
#
# <%= semantic_form_for @post do |f| %>
# <%= f.inputs do %>
# <%= f.input :title %>
# <%= f.input :body %>
# <% end %>
# <% end %>
#
# The HTML output will be something like:
#
# <form class="formtastic" method="post" action="...">
# <fieldset>
# <ol>
# <li class="string required" id="post_title_input">
# ...
# </li>
# <li class="text required" id="post_body_input">
# ...
# </li>
# </ol>
# </fieldset>
# </form>
#
# It's important to note that the `semantic_form_for` and {#inputs} blocks wrap the
# standard Rails `form_for` helper and FormBuilder, so you have full access to every standard
# Rails form helper, with any HTML markup and ERB syntax, allowing you to "break free" from
# Formtastic when it doesn't suit:
#
# <%= semantic_form_for @post do |f| %>
# <%= f.inputs do %>
# <%= f.input :title %>
# <li>
# <%= f.text_area :body %>
# <li>
# <% end %>
# <% end %>
#
# @see Formtastic::Helpers::InputHelper#input
module InputsHelper
include Formtastic::Helpers::FieldsetWrapper
include Formtastic::LocalizedString
# {#inputs} creates an input fieldset and ol tag wrapping for use around a set of inputs. It can be
# called either with a block (in which you can do the usual Rails form stuff, HTML, ERB, etc),
# or with a list of fields (accepting all default arguments and options). These two examples
# are functionally equivalent:
#
# # With a block:
# <% semantic_form_for @post do |form| %>
# <% f.inputs do %>
# <%= f.input :title %>
# <%= f.input :body %>
# <% end %>
# <% end %>
#
# # With a list of fields (short hand syntax):
# <% semantic_form_for @post do |form| %>
# <%= f.inputs :title, :body %>
# <% end %>
#
# # Output:
# <form ...>
# <fieldset class="inputs">
# <ol>
# <li class="string">...</li>
# <li class="text">...</li>
# </ol>
# </fieldset>
# </form>
#
# **Quick Forms**
#
# Quick, scaffolding-style forms can be easily rendered for rapid early development if called
# without a block or a field list. In the case an input is rendered for **most** columns in
# the model's database table (like Rails' scaffolding) plus inputs for some model associations.
#
# In this case, all inputs are rendered with default options and arguments. You'll want more
# control than this in a production application, but it's a great way to get started, then
# come back later to customise the form with a field list or a block of inputs. Example:
#
# <% semantic_form_for @post do |form| %>
# <%= f.inputs %>
# <% end %>
#
# **Nested Attributes**
#
# One of the most complicated parts of Rails forms comes when nesting the inputs for
# attrinbutes on associated models. Formtastic can take the pain away for many (but not all)
# situations.
#
# Given the following models:
#
# # Models
# class User < ActiveRecord::Base
# has_one :profile
# accepts_nested_attributes_for :profile
# end
# class Profile < ActiveRecord::Base
# belongs_to :user
# end
#
# Formtastic provides a helper called `semantic_fields_for`, which wraps around Rails' built-in
# `fields_for` helper for backwards compatibility with previous versions of Formtastic, and for
# a consistent method naming API. The following examples are functionally equivalent:
#
# <% semantic_form_for @user do |form| %>
# <%= f.inputs :name, :email %>
#
# <% f.semantic_fields_for :profile do |profile| %>
# <% profile.inputs do %>
# <%= profile.input :biography %>
# <%= profile.input :twitter_name %>
# <% end %>
# <% end %>
# <% end %>
#
# <% semantic_form_for @user do |form| %>
# <%= f.inputs :name, :email %>
#
# <% f.fields_for :profile do |profile| %>
# <% profile.inputs do %>
# <%= profile.input :biography %>
# <%= profile.input :twitter_name %>
# <% end %>
# <% end %>
# <% end %>
#
# {#inputs} also provides a DSL similar to `fields_for` / `semantic_fields_for` to reduce the
# lines of code a little:
#
# <% semantic_form_for @user do |f| %>
# <%= f.inputs :name, :email %>
#
# <% f.inputs :for => :profile do %>
# <%= profile.input :biography %>
# <%= profile.input :twitter_name %>
# <%= profile.input :shoe_size %>
# <% end %>
# <% end %>
#
# The `:for` option also works with short hand syntax:
#
# <% semantic_form_for @post do |form| %>
# <%= f.inputs :name, :email %>
# <%= f.inputs :biography, :twitter_name, :shoe_size, :for => :profile %>
# <% end %>
#
# {#inputs} will always create a new `<fieldset>` wrapping, so only use it when it makes sense
# in the document structure and semantics (using `semantic_fields_for` otherwise).
#
# All options except `:name`, `:title` and `:for` will be passed down to the fieldset as HTML
# attributes (id, class, style, etc).
#
# When nesting `inputs()` inside another `inputs()` block, the nested content will
# automatically be wrapped in an `<li>` tag to preserve the HTML validity (a `<fieldset>`
# cannot be a direct descendant of an `<ol>`.
#
#
# @option *args :for [Symbol, ActiveModel, Array]
# The contents of this option is passed down to Rails' fields_for() helper, so it accepts the same values.
#
# @option *args :name [String]
# The optional name passed into the `<legend>` tag within the fieldset (alias of `:title`)
#
# @option *args :title [String]
# The optional name passed into the `<legend>` tag within the fieldset (alias of `:name`)
#
#
# @example Quick form: Render a scaffold-like set of inputs for automatically guessed attributes and simple associations on the model, with all default arguments and options
# <% semantic_form_for @post do |form| %>
# <%= f.inputs %>
# <% end %>
#
# @example Quick form: Skip one or more fields
# <%= f.inputs :except => [:featured, :something_for_admin_only] %>
# <%= f.inputs :except => :featured %>
#
# @example Short hand: Render inputs for a named set of attributes and simple associations on the model, with all default arguments and options
# <% semantic_form_for @post do |form| %>
# <%= f.inputs :title, :body, :user, :categories %>
# <% end %>
#
# @example Block: Render inputs for attributes and simple associations with full control over arguments and options
# <% semantic_form_for @post do |form| %>
# <%= f.inputs do %>
# <%= f.input :title ... %>
# <%= f.input :body ... %>
# <%= f.input :user ... %>
# <%= f.input :categories ... %>
# <% end %>
# <% end %>
#
# @example Multiple blocks: Render inputs in multiple fieldsets
# <% semantic_form_for @post do |form| %>
# <%= f.inputs do %>
# <%= f.input :title ... %>
# <%= f.input :body ... %>
# <% end %>
# <%= f.inputs do %>
# <%= f.input :user ... %>
# <%= f.input :categories ... %>
# <% end %>
# <% end %>
#
# @example Provide text for the `<legend>` to name a fieldset (with a block)
# <% semantic_form_for @post do |form| %>
# <%= f.inputs :name => 'Write something:' do %>
# <%= f.input :title ... %>
# <%= f.input :body ... %>
# <% end %>
# <%= f.inputs do :name => 'Advanced options:' do %>
# <%= f.input :user ... %>
# <%= f.input :categories ... %>
# <% end %>
# <% end %>
#
# @example Provide text for the `<legend>` to name a fieldset (with short hand)
# <% semantic_form_for @post do |form| %>
# <%= f.inputs :title, :body, :name => 'Write something:'%>
# <%= f.inputs :user, :cateogies, :name => 'Advanced options:' %>
# <% end %>
#
# @example Inputs for nested attributes (don't forget `accepts_nested_attributes_for` in your model, see Rails' `fields_for` documentation)
# <% semantic_form_for @user do |form| %>
# <%= f.inputs do %>
# <%= f.input :name ... %>
# <%= f.input :email ... %>
# <% end %>
# <%= f.inputs :for => :profile do |profile| %>
# <%= profile.input :user ... %>
# <%= profile.input :categories ... %>
# <% end %>
# <% end %>
#
# @example Inputs for nested record (don't forget `accepts_nested_attributes_for` in your model, see Rails' `fields_for` documentation)
# <% semantic_form_for @user do |form| %>
# <%= f.inputs do %>
# <%= f.input :name ... %>
# <%= f.input :email ... %>
# <% end %>
# <%= f.inputs :for => @user.profile do |profile| %>
# <%= profile.input :user ... %>
# <%= profile.input :categories ... %>
# <% end %>
# <% end %>
#
# @example Inputs for nested record with a different name (don't forget `accepts_nested_attributes_for` in your model, see Rails' `fields_for` documentation)
# <% semantic_form_for @user do |form| %>
# <%= f.inputs do %>
# <%= f.input :name ... %>
# <%= f.input :email ... %>
# <% end %>
# <%= f.inputs :for => [:user_profile, @user.profile] do |profile| %>
# <%= profile.input :user ... %>
# <%= profile.input :categories ... %>
# <% end %>
# <% end %>
#
# @example Nesting {#inputs} blocks requires an extra `<li>` tag for valid markup
# <% semantic_form_for @user do |form| %>
# <%= f.inputs do %>
# <%= f.input :name ... %>
# <%= f.input :email ... %>
# <li>
# <%= f.inputs :for => [:user_profile, @user.profile] do |profile| %>
# <%= profile.input :user ... %>
# <%= profile.input :categories ... %>
# <% end %>
# </li>
# <% end %>
# <% end %>
def inputs(*args, &block)
wrap_it = @already_in_an_inputs_block ? true : false
@already_in_an_inputs_block = true
title = field_set_title_from_args(*args)
html_options = args.extract_options!
html_options[:class] ||= "inputs"
html_options[:name] = title
skipped_args = Array.wrap html_options.delete(:except)
out = begin
if html_options[:for] # Nested form
inputs_for_nested_attributes(*(args << html_options), &block)
elsif block_given?
field_set_and_list_wrapping(*(args << html_options), &block)
else
legend = args.shift if args.first.is_a?(::String)
args = default_columns_for_object - skipped_args if @object && args.empty?
contents = fieldset_contents_from_column_list(args)
args.unshift(legend) if legend.present?
field_set_and_list_wrapping(*((args << html_options) << contents))
end
end
out = template.content_tag(:li, out, :class => "input") if wrap_it
@already_in_an_inputs_block = wrap_it
out
end
protected
def default_columns_for_object
cols = association_columns(:belongs_to)
cols += content_columns
cols -= Formtastic::FormBuilder.skipped_columns
cols.compact
end
def fieldset_contents_from_column_list(columns)
columns.collect do |method|
if @object
if @object.class.respond_to?(:reflect_on_association)
if (@object.class.reflect_on_association(method.to_sym) && @object.class.reflect_on_association(method.to_sym).options[:polymorphic] == true)
raise PolymorphicInputWithoutCollectionError.new("Please provide a collection for :#{method} input (you'll need to use block form syntax). Inputs for polymorphic associations can only be used when an explicit :collection is provided.")
end
elsif @object.class.respond_to?(:associations)
if (@object.class.associations[method.to_sym] && @object.class.associations[method.to_sym].options[:polymorphic] == true)
raise PolymorphicInputWithoutCollectionError.new("Please provide a collection for :#{method} input (you'll need to use block form syntax). Inputs for polymorphic associations can only be used when an explicit :collection is provided.")
end
end
end
input(method.to_sym)
end
end
# Collects association columns (relation columns) for the current form object class. Skips
# polymorphic associations because we can't guess which class to use for an automatically
# generated input.
def association_columns(*by_associations) # @private
if @object.present? && @object.class.respond_to?(:reflections)
@object.class.reflections.collect do |name, association_reflection|
if by_associations.present?
if by_associations.include?(association_reflection.macro) && association_reflection.options[:polymorphic] != true
name
end
else
name
end
end.compact
else
[]
end
end
# Collects all foreign key columns
def foreign_key_columns # @private
if @object.present? && @object.class.respond_to?(:reflect_on_all_associations)
@object.class.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
else
[]
end
end
# Collects content columns (non-relation columns) for the current form object class.
def content_columns # @private
# TODO: NameError is raised by Inflector.constantize. Consider checking if it exists instead.
begin klass = model_name.constantize; rescue NameError; return [] end
return [] unless klass.respond_to?(:content_columns)
klass.content_columns.collect { |c| c.name.to_sym }.compact - foreign_key_columns
end
# Deals with :for option when it's supplied to inputs methods. Additional
# options to be passed down to :for should be supplied using :for_options
# key.
#
# It should raise an error if a block with arity zero is given.
def inputs_for_nested_attributes(*args, &block) # @private
options = args.extract_options!
args << options.merge!(:parent => { :builder => self, :for => options[:for] })
fields_for_block = if block_given?
raise ArgumentError, 'You gave :for option with a block to inputs method, ' <<
'but the block does not accept any argument.' if block.arity <= 0
lambda do |f|
contents = f.inputs(*args) do
if block.arity == 1 # for backwards compatibility with REE & Ruby 1.8.x
yield(f)
else
index = parent_child_index(options[:parent]) if options[:parent]
yield(f, index)
end
end
template.concat(contents)
end
else
lambda do |f|
contents = f.inputs(*args)
template.concat(contents)
end
end
fields_for_args = [options.delete(:for), options.delete(:for_options) || {}].flatten(1)
fields_for(*fields_for_args, &fields_for_block)
end
def field_set_title_from_args(*args) # @private
options = args.extract_options!
options[:name] ||= options.delete(:title)
title = options[:name]
if title.blank?
valid_name_classes = [::String, ::Symbol]
valid_name_classes.delete(::Symbol) if !block_given? && (args.first.is_a?(::Symbol) && content_columns.include?(args.first))
title = args.shift if valid_name_classes.any? { |valid_name_class| args.first.is_a?(valid_name_class) }
end
title = localized_string(title, title, :title) if title.is_a?(::Symbol)
title
end
end
end
end