/
form_builder.rb
382 lines (349 loc) · 15.3 KB
/
form_builder.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
module AirBlade
module AirBudd
class FormBuilder < ActionView::Helpers::FormBuilder
include Haml::Helpers if defined? Haml # for compatibility
include ActionView::Helpers::TextHelper # so we can use concat
include ActionView::Helpers::CaptureHelper # so we can use capture
# App-wide form configuration.
# E.g. in config/initializers/form_builder.rb:
#
# AirBlade::AirBudd::FormBuilder.default_options[:required_signifier] = '*'
#
@@default_options = {
:required_signifier => '(required)',
:label_suffix => ':',
:capitalize_errors => true,
}
cattr_accessor :default_options
# Per-form configuration (overrides app-wide form configuration).
# E.g. in a form itself:
#
# - airbudd_form_for @member do |f|
# - f.required_signifier = '*'
# = f.text_field :name
# ...etc...
#
attr_writer *default_options.keys
default_options.keys.each do |field|
src = <<-END_SRC
def #{field}
@#{field} || default_options[:#{field}]
end
END_SRC
class_eval src, __FILE__, __LINE__
end
@@field_keys = [:hint, :required, :label, :addendum, :suffix]
# We make copies (by aliasing) of ActionView::Helpers::FormBuilder's
# vanilla text_field and select methods, so we can use them in our
# latitude_field and longitude_field methods.
#
# NOTE: these alias_methods must come before we override the text_field
# and select methods.
#
# NOTE: this could be implemented more safely using Ara T Howard's technique,
# described here:
#
# http://blog.airbladesoftware.com/2008/1/17/note-to-self-overriding-a-method-with-a-mixin
#
# See also the techniques described by Jay Fields here:
#
# http://blog.jayfields.com/2008/04/alternatives-for-redefining-methods.html
alias_method :vanilla_text_field, :text_field
alias_method :vanilla_hidden_field, :hidden_field
alias_method :vanilla_select, :select
# Creates a glorified form field helper. It takes a form helper's usual
# arguments with an optional options hash:
#
# <%= form.text_field 'title',
# :required => true,
# :label => "Article's Title",
# :hint => "Try not to use the letter 'e'." %>
#
# The code above generates the following HTML. The :required entry in the hash
# triggers the <em/> element and the :label overwrites the default field label,
# 'title' in this case, with its value. The stanza is wrapped in a <p/> element.
#
# <p class="text">
# <label for="article_title">Article's Title:
# <em class="required">(required)</em>
# </label>
# <input id="article_title" name="article[title]" type="text" value=""/>
# <span class="hint">Try not to use the letter 'e'.</span>
# </p>
#
# If the field's value is invalid, the <p/> is marked so and a <span/> is added
# with the (in)validation message:
#
# <p class="error text">
# <label for="article_title">Article's Title:
# <em class="required">(required)</em>
# <span class="feedback">can't be blank</span>
# </label>
# <input id="article_title" name="article[title]" type="text" value=""/>
# <span class="hint">Try not to use the letter 'e'.</span>
# </p>
#
# You can also pass an :addendum option. This generates a <span/> between the
# <input/> and the hint. Typically you would use this to show a small icon
# for deleting the field.
def self.create_field_helper(field_helper)
src = <<-END
def #{field_helper}(method, options = {}, html_options = {})
@template.content_tag('p',
label_element(method, options, html_options) +
super(method, options.except(*@@field_keys)) +
addendum_element(options) +
hint_element(options),
attributes_for(method, '#{field_helper}')
)
end
END
class_eval src, __FILE__, __LINE__
end
def self.create_short_field_helper(field_helper)
src = <<-END
def #{field_helper}(method, options = {}, html_options = {})
@template.content_tag('p',
super(method, options.except(*@@field_keys)) +
label_element(method, options, html_options) +
hint_element(options),
attributes_for(method, '#{field_helper}')
)
end
END
class_eval src, __FILE__, __LINE__
end
# Creates a hidden input field and a simple <span/> using the same
# pattern as other form fields.
def read_only_text_field(method_for_text_field, method_for_hidden_field = nil, options = {}, html_options = {})
method_for_hidden_field ||= method_for_text_field
@template.content_tag('p',
label_element(method_for_text_field, options, html_options) +
vanilla_hidden_field(method_for_hidden_field, options) +
@template.content_tag('span', object.send(method_for_text_field)) +
addendum_element(options) +
hint_element(options),
attributes_for(method_for_text_field, 'text_field')
)
end
# TODO: DRY this with self.create_field_helper above.
def self.create_collection_field_helper(field_helper)
src = <<-END
def #{field_helper}(method, choices, options = {}, html_options = {})
@template.content_tag('p',
label_element(method, options, html_options) +
super(method, choices, options.except(*@@field_keys)) +
addendum_element(options) +
hint_element(options),
attributes_for(method, '#{field_helper}')
)
end
END
class_eval src, __FILE__, __LINE__
end
def attributes_for(method, field_helper)
# FIXME: there must be a neater way than below. This is Ruby, after all.
ary = []
ary << 'error' if errors_for?(method)
ary << input_type_for(field_helper) unless input_type_for(field_helper).blank?
attrs = {}
attrs[:class] = ary.reject{ |x| x.blank? }.join(' ') unless ary.empty?
attrs
end
def input_type_for(field_helper)
case field_helper
when 'text_field'; 'text'
when 'text_area'; 'text'
when 'password_field'; 'password'
when 'file_field'; 'file'
when 'hidden_field'; 'hidden'
when 'check_box'; 'checkbox'
when 'radio_button'; 'radio'
when 'select'; 'select'
when 'date_select'; 'select'
when 'time_select'; 'select'
when 'country_select'; 'select'
else ''
end
end
# Beefs up the appropriate field helpers.
%w( text_field text_area password_field file_field
date_select time_select country_select ).each do |name|
create_field_helper name
end
# Beefs up the appropriate field helpers.
%w( check_box radio_button ).each do |name|
create_short_field_helper name
end
# Beefs up the appropriate field helpers.
%w( select ).each do |name|
create_collection_field_helper name
end
# Support for GeoTools.
# http://opensource.airbladesoftware.com/trunk/plugins/geo_tools/
def latitude_field(method, options = {}, html_options = {})
@template.content_tag('p',
label_element(method, options, html_options) + (
vanilla_text_field("#{method}_degrees", options.merge(:maxlength => 2)) + '°' +
vanilla_text_field("#{method}_minutes", options.merge(:maxlength => 2)) + '.' +
vanilla_text_field("#{method}_milli_minutes", options.merge(:maxlength => 3)) + '′' +
# Hmm, we pass the options in the html_options position.
vanilla_select("#{method}_hemisphere", %w( N S ), {}, options)
) +
hint_element(options),
(errors_for?(method) ? {:class => 'error'} : {})
)
end
# Support for GeoTools.
# http://opensource.airbladesoftware.com/trunk/plugins/geo_tools/
def longitude_field(method, options = {}, html_options = {})
@template.content_tag('p',
label_element(method, options, html_options) + (
vanilla_text_field("#{method}_degrees", options.merge(:maxlength => 3)) + '°' +
vanilla_text_field("#{method}_minutes", options.merge(:maxlength => 2)) + '.' +
vanilla_text_field("#{method}_milli_minutes", options.merge(:maxlength => 3)) + '′' +
# Hmm, we pass the options in the html_options position.
vanilla_select("#{method}_hemisphere", %w( E W ), {}, options)
) +
hint_element(options),
(errors_for?(method) ? {:class => 'error'} : {})
)
end
# Within the form's block you can get good buttons with:
#
# <% f.buttons do |b| %>
# <%= b.save %>
# <%= b.cancel %>
# <% end %>
#
# You can have save, cancel, edit and delete buttons.
# Each one takes an optional label. For example:
#
# <%= b.save :label => 'Update' %>
#
# See the documentation for the +button+ method for the
# options you can use.
#
# You could call the button method directly, e.g. <%= f.button %>,
# but then your button would not be wrapped with a div of class
# 'buttons'. The div is needed for the CSS.
def buttons(&block)
content = capture(self, &block)
concat '<div class="buttons">', block.binding
concat content, block.binding
concat '</div>', block.binding
end
# Buttons and links for REST actions. Actions that change
# state, i.e. save and delete, have buttons. Other actions
# have links.
#
# For visual feedback with colours and icons, save is seen
# as a positive action; delete is negative.
#
# type = :new|:save|:cancel|:edit|:delete
# TODO :all ?
#
# Options you can use are:
# :label - The label for the button or text for the link.
# Optional; defaults to capitalised purpose.
# :icon - Whether or not to show an icon to the left of the label.
# Optional; icon will be shown unless :icon set to false.
# :url - The URL to link to (only used in links).
# Optional; defaults to ''.
def button(purpose = :save, options = {}, html_options = {})
# TODO: DRY the :a and :button.
element, icon, nature = case purpose
when :new then [:a, 'add', 'positive']
when :save then [:button, 'tick', 'positive']
when :cancel then [:a, 'arrow_undo', nil ]
when :edit then [:a, 'pencil', nil ]
when :delete then [:button, 'cross', 'negative']
end
legend = ( (options[:icon] == false || options[:icon] == 'false') ?
'' :
"<img src='/images/icons/#{icon}.png' alt=''/> " ) +
(options[:label] || purpose.to_s.capitalize)
html_options.merge!(:class => nature)
if element == :button
html_options.merge!(:type => 'submit')
else
html_options.merge!(:href => (options[:url] || ''))
end
# TODO: separate button and link construction and use
# link_to to gain its functionality, e.g. :back?
@template.content_tag(element.to_s,
legend,
html_options)
end
def method_missing(*args, &block)
# Button method
if args.first.to_s =~ /^(new|save|cancel|edit|delete)$/
button args.shift, *args, &block
else
super
end
end
private
# Writes out a <label/> element for the given field.
# Options:
# - :required: text to indicate that field is required. Optional: if not given,
# field is not required. If set to true instead of a string, default indicator
# text is '(required)'.
# - :label: text wrapped by the <label/>. Optional (default is field's name).
# - :suffix: appended to the label. Optional (default is ':').
# - :capitalize: false if any error message should not be capitalised,
# true otherwise. Optional (default is true).
def label_element(field, options = {}, html_options = {})
return '' if options.has_key?(:label) && options[:label].nil?
text = options.delete(:label) || field.to_s.humanize
suffix = options.delete(:suffix) || label_suffix
value = text + suffix
if (required = mandatory?(field, options.delete(:required)))
required = required_signifier if required == true
value += " <em class='required'>#{required}</em>"
end
html_options.stringify_keys!
html_options['for'] ||= "#{@object_name}_#{field}"
if errors_for? field
error_msg = @object.errors[field].to_a.to_sentence
option_capitalize = options.delete(:capitalize) || capitalize_errors
error_msg = error_msg.capitalize unless option_capitalize == 'false' or option_capitalize == false
value += %Q( <span class="feedback">#{error_msg}.</span>)
end
@template.content_tag :label, value, html_options
end
def mandatory?(method, override = nil)
return override unless override.nil?
# Leverage vendor/validation_reflection.rb
if @object.class.respond_to? :reflect_on_validations_for
@object.class.reflect_on_validations_for(method).any? { |v| v.macro == :validates_presence_of }
end
end
# Writes out a <span/> element with a hint for how to fill in a field.
# Options:
# - :hint: text for the hint. Optional.
def hint_element(options = {})
hint = options.delete :hint
if hint
@template.content_tag :span, hint, :class => 'hint'
else
''
end
end
# Writes out a <span/> element with something that follows a field.
# Options:
# - :hint: text for the hint. Optional.
def addendum_element(options = {})
addendum = options.delete :addendum
if addendum
@template.content_tag :span, addendum, :class => 'addendum'
else
''
end
end
def errors_for?(method)
@object && @object.respond_to?(:errors) && @object.errors[method]
end
end
end
end