form_assistant 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/form_assistant.gemspec +4 -4
- data/lib/form_assistant.rb +6 -374
- data/lib/form_assistant/form_builder.rb +339 -0
- data/lib/form_assistant/view_helpers.rb +44 -0
- data/test/test_form_assistant.rb +5 -1
- metadata +6 -6
- data/init.rb +0 -2
- data/install.rb +0 -1
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.2.0
|
data/form_assistant.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{form_assistant}
|
8
|
-
s.version = "1.
|
8
|
+
s.version = "1.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Ryan Heath", "Chris Scharf"]
|
12
|
-
s.date = %q{2012-07-
|
12
|
+
s.date = %q{2012-07-07}
|
13
13
|
s.description = %q{Custom form builder that attempts to make your forms friendly}
|
14
14
|
s.email = %q{scharfie@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -34,12 +34,12 @@ Gem::Specification.new do |s|
|
|
34
34
|
"forms/_radio_button.html.erb",
|
35
35
|
"forms/_text_area.html.erb",
|
36
36
|
"forms/_text_field.html.erb",
|
37
|
-
"init.rb",
|
38
|
-
"install.rb",
|
39
37
|
"lib/form_assistant.rb",
|
40
38
|
"lib/form_assistant/error.rb",
|
41
39
|
"lib/form_assistant/field_errors.rb",
|
40
|
+
"lib/form_assistant/form_builder.rb",
|
42
41
|
"lib/form_assistant/rules.rb",
|
42
|
+
"lib/form_assistant/view_helpers.rb",
|
43
43
|
"tasks/form_assistant_tasks.rake",
|
44
44
|
"test/forms/_field.html.erb",
|
45
45
|
"test/forms/_fieldset.html.erb",
|
data/lib/form_assistant.rb
CHANGED
@@ -1,375 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require 'form_assistant/error'
|
2
|
+
require 'form_assistant/field_errors'
|
3
|
+
require 'form_assistant/rules'
|
4
|
+
require 'form_assistant/form_builder'
|
5
|
+
require 'form_assistant/view_helpers'
|
4
6
|
|
5
|
-
|
6
|
-
module RPH
|
7
|
-
# The idea is to make forms extremely less painful and a lot more DRY
|
8
|
-
module FormAssistant
|
9
|
-
FORM_HELPERS = [
|
10
|
-
ActionView::Helpers::FormBuilder.field_helpers +
|
11
|
-
%w(date_select datetime_select time_select collection_select select country_select time_zone_select) -
|
12
|
-
%w(hidden_field label fields_for)
|
13
|
-
].flatten.freeze
|
14
|
-
|
15
|
-
# FormAssistant::FormBuilder
|
16
|
-
# * provides several convenient helpers (see helpers.rb) and
|
17
|
-
# an infrastructure to easily add your own
|
18
|
-
# * method_missing hook to wrap content "on the fly"
|
19
|
-
# * optional: automatically attach labels to field helpers
|
20
|
-
# * optional: format fields using partials (extremely extensible)
|
21
|
-
#
|
22
|
-
# Usage:
|
23
|
-
#
|
24
|
-
# <% form_for @project, :builder => RPH::FormAssistant::FormBuilder do |form| %>
|
25
|
-
# // fancy form stuff
|
26
|
-
# <% end %>
|
27
|
-
#
|
28
|
-
# - or -
|
29
|
-
#
|
30
|
-
# <% form_assistant_for @project do |form| %>
|
31
|
-
# // fancy form stuff
|
32
|
-
# <% end %>
|
33
|
-
#
|
34
|
-
# - or -
|
35
|
-
#
|
36
|
-
# # in config/intializers/form_assistant.rb
|
37
|
-
# ActionView::Base.default_form_builder = RPH::FormAssistant::FormBuilder
|
38
|
-
class FormBuilder < ActionView::Helpers::FormBuilder
|
39
|
-
# used if no other template is available
|
40
|
-
attr_accessor :fallback_template
|
41
|
-
|
42
|
-
# override the field_error_proc so that it no longer wraps the field
|
43
|
-
# with <div class="fieldWithErrors">...</div>, but just returns the field
|
44
|
-
ActionView::Base.field_error_proc = Proc.new { |html_tag, instance| html_tag }
|
45
|
-
|
46
|
-
class << self
|
47
|
-
attr_accessor :ignore_templates
|
48
|
-
attr_accessor :ignore_labels
|
49
|
-
attr_accessor :ignore_errors
|
50
|
-
attr_accessor :template_root
|
51
|
-
|
52
|
-
# if set to true, none of the templates will be used;
|
53
|
-
# however, labels can still be automatically attached
|
54
|
-
# and all FormAssistant helpers are still avaialable
|
55
|
-
@ignore_templates = false
|
56
|
-
|
57
|
-
# if set to true, labels will become nil everywhere (both
|
58
|
-
# with and without templates)
|
59
|
-
@ignore_labels = false
|
60
|
-
|
61
|
-
# set to true if you'd rather use #error_messages_for()
|
62
|
-
@ignore_errors = false
|
63
|
-
|
64
|
-
# sets the root directory where templates will be searched
|
65
|
-
# note: the template root should be nested within the
|
66
|
-
# configured view path (which defaults to app/views)
|
67
|
-
def template_root(full_path = false)
|
68
|
-
@template_root ||= File.join(Rails.configuration.view_path, 'forms')
|
69
|
-
|
70
|
-
# render(:partial => '...') doesn't want the full path of the template
|
71
|
-
full_path ? @template_root : @template_root.gsub(Rails.configuration.view_path + '/', '')
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
# get the error messages (if any) for a field
|
77
|
-
def error_message_for(fields)
|
78
|
-
errors = []
|
79
|
-
fields = [fields] unless Array === fields
|
80
|
-
|
81
|
-
fields.each do |field|
|
82
|
-
next unless has_errors?(field)
|
83
|
-
|
84
|
-
errors += if RPH::FormAssistant::Rules.has_I18n_support?
|
85
|
-
full_messages_for(field)
|
86
|
-
else
|
87
|
-
human_field_name = field.to_s.humanize
|
88
|
-
errors += [*object.errors[field]].map do |error|
|
89
|
-
"#{human_field_name} #{error}"
|
90
|
-
end
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
errors.empty? ? nil : RPH::FormAssistant::FieldErrors.new(errors)
|
95
|
-
end
|
96
|
-
|
97
|
-
# Returns full error messages for given field (uses I18n)
|
98
|
-
def full_messages_for(field)
|
99
|
-
attr_name = object.class.human_attribute_name(field.to_s)
|
100
|
-
|
101
|
-
object.errors[field].inject([]) do |full_messages, message|
|
102
|
-
next unless message
|
103
|
-
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
|
104
|
-
end
|
105
|
-
end
|
106
|
-
|
107
|
-
# returns true if a field is invalid
|
108
|
-
def has_errors?(field)
|
109
|
-
!(object.nil? || object.errors[field].blank?)
|
110
|
-
end
|
111
|
-
|
112
|
-
# checks to make sure the template exists
|
113
|
-
def template_exists?(template)
|
114
|
-
File.exists?(File.join(self.class.template_root(true), "_#{template}.html.erb"))
|
115
|
-
end
|
116
|
-
|
117
|
-
protected
|
118
|
-
# renders the appropriate partial located in the template root
|
119
|
-
def render_partial_for(element, field, label, tip, template, helper, required, extra_locals, args)
|
120
|
-
errors = self.class.ignore_errors ? nil : error_message_for(field)
|
121
|
-
locals = (extra_locals || {}).merge(:element => element, :field => field, :builder => self, :object => object, :object_name => object_name, :label => label, :errors => errors, :tip => tip, :helper => helper, :required => required)
|
122
|
-
|
123
|
-
@template.render :partial => "#{self.class.template_root}/#{template}.html.erb", :locals => locals
|
124
|
-
end
|
125
|
-
|
126
|
-
# render the element with an optional label (does not use the templates)
|
127
|
-
def render_element(element, field, name, options, ignore_label = false)
|
128
|
-
return element if ignore_label
|
129
|
-
|
130
|
-
# need to consider if the shortcut label option was used
|
131
|
-
# i.e. <%= form.text_field :title, :label => 'Project Title' %>
|
132
|
-
text, label_options = if options[:label].is_a?(String)
|
133
|
-
[options.delete(:label), {}]
|
134
|
-
else
|
135
|
-
[options[:label].delete(:text), options.delete(:label)]
|
136
|
-
end
|
137
|
-
|
138
|
-
# consider trailing labels
|
139
|
-
if %w(check_box radio_button).include?(name)
|
140
|
-
label_options[:class] = (label_options[:class].to_s + ' inline').strip
|
141
|
-
element + label(field, text, label_options)
|
142
|
-
else
|
143
|
-
label(field, text, label_options) + element
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def extract_options_for_label(field, options={})
|
148
|
-
label_options = {}
|
149
|
-
|
150
|
-
# consider the global setting for labels and
|
151
|
-
# allow for turning labels off on a per-helper basis
|
152
|
-
# <%= form.text_field :title, :label => false %>
|
153
|
-
if self.class.ignore_labels || options[:label] === false || field.blank?
|
154
|
-
label_options[:label] = false
|
155
|
-
else
|
156
|
-
# ensure that the :label option is a Hash from this point on
|
157
|
-
options[:label] ||= {}
|
158
|
-
|
159
|
-
# allow for a cleaner way of setting label text
|
160
|
-
# <%= form.text_field :whatever, :label => 'Whatever Title' %>
|
161
|
-
label_options.merge!(options[:label].is_a?(String) ? {:text => options[:label]} : options[:label])
|
162
|
-
|
163
|
-
# allow for a more convenient way to set common label options
|
164
|
-
# <%= form.text_field :whatever, :label_id => 'dom_id' %>
|
165
|
-
# <%= form.text_field :whatever, :label_class => 'required' %>
|
166
|
-
# <%= form.text_field :whatever, :label_text => 'Whatever' %>
|
167
|
-
%w(id class text).each do |option|
|
168
|
-
label_option = "label_#{option}".to_sym
|
169
|
-
label_options.merge!(option.to_sym => options.delete(label_option)) if options[label_option]
|
170
|
-
end
|
171
|
-
|
172
|
-
# Ensure we have default label text
|
173
|
-
# (since Rails' label() does not currently respect I18n)
|
174
|
-
label_options[:text] ||= object.class.human_attribute_name(field.to_s)
|
175
|
-
end
|
176
|
-
|
177
|
-
label_options
|
178
|
-
end
|
179
|
-
|
180
|
-
def extract_options_for_template(helper_name, options={})
|
181
|
-
template_options = {}
|
182
|
-
|
183
|
-
if options.has_key?(:template) && options[:template].kind_of?(FalseClass)
|
184
|
-
template_options[:template] = false
|
185
|
-
else
|
186
|
-
# grab the template
|
187
|
-
template = options.delete(:template) || helper_name.to_s
|
188
|
-
template = self.fallback_template unless template_exists?(template)
|
189
|
-
template_options[:template] = template
|
190
|
-
end
|
191
|
-
|
192
|
-
template_options
|
193
|
-
end
|
194
|
-
|
195
|
-
public
|
196
|
-
def fallback_template
|
197
|
-
@fallback_template ||= 'field'
|
198
|
-
end
|
199
|
-
|
200
|
-
def self.assist(helper_name)
|
201
|
-
define_method(helper_name) do |field, *args|
|
202
|
-
options = (helper_name == 'check_box' ? args.shift : args.extract_options!) || {}
|
203
|
-
label_options = extract_options_for_label(field, options)
|
204
|
-
template_options = extract_options_for_template(helper_name, options)
|
205
|
-
extra_locals = options.delete(:locals) || {}
|
206
|
-
|
207
|
-
# build out the label element (if desired)
|
208
|
-
label = label_options[:label] === false ? nil : self.label(field, label_options.delete(:text), label_options)
|
209
|
-
|
210
|
-
# grab the tip, if any
|
211
|
-
tip = options.delete(:tip)
|
212
|
-
|
213
|
-
# is the field required?
|
214
|
-
required = !!options.delete(:required)
|
215
|
-
|
216
|
-
# ensure that we don't have any custom options pass through
|
217
|
-
field_options = options.except(:label, :template, :tip, :required)
|
218
|
-
|
219
|
-
# call the original render for the element
|
220
|
-
super_args = helper_name == 'check_box' ? args.unshift(field_options) : args.push(field_options)
|
221
|
-
element = super(field, *super_args)
|
222
|
-
|
223
|
-
return element if template_options[:template] === false
|
224
|
-
|
225
|
-
# return the helper with an optional label if templates are not to be used
|
226
|
-
return render_element(element, field, helper_name, options, label_options[:label] === false) if self.class.ignore_templates
|
227
|
-
|
228
|
-
# render the partial template from the desired template root
|
229
|
-
render_partial_for(element, field, label, tip, template_options[:template], helper_name, required, extra_locals, args)
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
# redefining all traditional form helpers so that they
|
234
|
-
# behave the way FormAssistant thinks they should behave
|
235
|
-
RPH::FormAssistant::FORM_HELPERS.each do |helper_name|
|
236
|
-
assist(helper_name)
|
237
|
-
end
|
238
|
-
|
239
|
-
def without_assistance(options={}, &block)
|
240
|
-
# TODO - allow options to only turn off templates and/or labels
|
241
|
-
ignore_labels, ignore_templates = self.class.ignore_labels, self.class.ignore_templates
|
242
|
-
|
243
|
-
begin
|
244
|
-
self.class.ignore_labels, self.class.ignore_templates = true, true
|
245
|
-
result = yield
|
246
|
-
ensure
|
247
|
-
self.class.ignore_labels, self.class.ignore_templates = ignore_labels, ignore_templates
|
248
|
-
end
|
249
|
-
|
250
|
-
result
|
251
|
-
end
|
252
|
-
|
253
|
-
def widget(*args, &block)
|
254
|
-
options = args.extract_options!
|
255
|
-
fields = args.shift || nil
|
256
|
-
field = Array === fields ? fields.first : fields
|
257
|
-
|
258
|
-
label_options = extract_options_for_label(field, options)
|
259
|
-
template_options = extract_options_for_template(self.fallback_template, options)
|
260
|
-
label = label_options[:label] === false ? nil : self.label(field, label_options.delete(:text), label_options)
|
261
|
-
tip = options.delete(:tip)
|
262
|
-
locals = options.delete(:locals)
|
263
|
-
required = !!options.delete(:required)
|
264
|
-
|
265
|
-
if block_given?
|
266
|
-
element = without_assistance do
|
267
|
-
@template.capture(&block)
|
268
|
-
end
|
269
|
-
else
|
270
|
-
element = nil
|
271
|
-
end
|
272
|
-
|
273
|
-
partial = render_partial_for(element, fields, label, tip, template_options[:template], 'widget', required, locals, args)
|
274
|
-
RPH::FormAssistant::Rules.binding_required? ? @template.concat(partial, block.binding) : @template.concat(partial)
|
275
|
-
end
|
276
|
-
|
277
|
-
# Renders a partial, passing the form object as a local
|
278
|
-
# variable named 'form'
|
279
|
-
# <%= form.partial 'shared/new', :locals => { :whatever => @whatever } %>
|
280
|
-
def partial(name, options={})
|
281
|
-
(options[:locals] ||= {}).update :form => self
|
282
|
-
options.update :partial => name
|
283
|
-
@template.render options
|
284
|
-
end
|
285
|
-
|
286
|
-
def input(field, *args)
|
287
|
-
helper_name = case column_type(field)
|
288
|
-
when :string
|
289
|
-
field.to_s.include?('password') ? :password_field : :text_field
|
290
|
-
when :text ; :text_area
|
291
|
-
when :integer, :float, :decimal ; :text_field
|
292
|
-
when :date ; :date_select
|
293
|
-
when :datetime, :timestamp ; :datetime_select
|
294
|
-
when :time ; :time_select
|
295
|
-
when :boolean ; :check_box
|
296
|
-
else ; :text_field
|
297
|
-
end
|
298
|
-
|
299
|
-
send(helper_name, field, *args)
|
300
|
-
end
|
301
|
-
|
302
|
-
def inputs(*args)
|
303
|
-
options = args.extract_options!
|
304
|
-
args.flatten.map do |field|
|
305
|
-
input(field, options.dup)
|
306
|
-
end.join('')
|
307
|
-
end
|
308
|
-
|
309
|
-
def column_type(field)
|
310
|
-
object.class.columns_hash[field.to_s].type rescue :string
|
311
|
-
end
|
312
|
-
|
313
|
-
# since #fields_for() doesn't inherit the builder from form_for, we need
|
314
|
-
# to provide a means to set the builder automatically (works with nesting, too)
|
315
|
-
#
|
316
|
-
# Usage: simply call #fields_for() on the builder object
|
317
|
-
#
|
318
|
-
# <% form_assistant_for @project do |form| %>
|
319
|
-
# <%= form.text_field :title %>
|
320
|
-
# <% form.fields_for :tasks do |task_fields| %>
|
321
|
-
# <%= task_fields.text_field :name %>
|
322
|
-
# <% end %>
|
323
|
-
# <% end %>
|
324
|
-
def fields_for_with_form_assistant(record_or_name_or_array, *args, &proc)
|
325
|
-
options = args.extract_options!
|
326
|
-
# hand control over to the original #fields_for()
|
327
|
-
fields_for_without_form_assistant(record_or_name_or_array, *(args << options.merge!(:builder => self.class)), &proc)
|
328
|
-
end
|
329
|
-
|
330
|
-
# used to intercept #fields_for() and set the builder
|
331
|
-
alias_method_chain :fields_for, :form_assistant
|
332
|
-
end
|
333
|
-
|
334
|
-
# methods that mix into ActionView::Base
|
335
|
-
module ActionView
|
336
|
-
private
|
337
|
-
# used to ensure that the desired builder gets set before calling #form_for()
|
338
|
-
def form_for_with_builder(record_or_name_or_array, builder, *args, &proc)
|
339
|
-
options = args.extract_options!
|
340
|
-
# hand control over to the original #form_for()
|
341
|
-
form_for(record_or_name_or_array, *(args << options.merge!(:builder => builder)), &proc)
|
342
|
-
end
|
343
|
-
|
344
|
-
# determines if binding is needed for #concat()
|
345
|
-
# (Rails 2.2.0 and greater no longer requires the binding)
|
346
|
-
def binding_required
|
347
|
-
RPH::FormAssistant::Rules.binding_required?
|
348
|
-
end
|
349
|
-
|
350
|
-
public
|
351
|
-
# easy way to make use of FormAssistant::FormBuilder
|
352
|
-
#
|
353
|
-
# <% form_assistant_for @project do |form| %>
|
354
|
-
# // fancy form stuff
|
355
|
-
# <% end %>
|
356
|
-
def form_assistant_for(record_or_name_or_array, *args, &proc)
|
357
|
-
form_for_with_builder(record_or_name_or_array, RPH::FormAssistant::FormBuilder, *args, &proc)
|
358
|
-
end
|
359
|
-
|
360
|
-
# (borrowed the #fieldset() helper from Chris Scharf:
|
361
|
-
# http://github.com/scharfie/slate/tree/master/app/helpers/application_helper.rb)
|
362
|
-
#
|
363
|
-
# <% fieldset 'User Registration' do %>
|
364
|
-
# // fields
|
365
|
-
# <% end %>
|
366
|
-
def fieldset(legend, &block)
|
367
|
-
locals = { :legend => legend, :fields => capture(&block) }
|
368
|
-
partial = render(:partial => "#{RPH::FormAssistant::FormBuilder.template_root}/fieldset.html.erb", :locals => locals)
|
369
|
-
|
370
|
-
# render the fields
|
371
|
-
binding_required ? concat(partial, block.binding) : concat(partial)
|
372
|
-
end
|
373
|
-
end
|
374
|
-
end
|
375
|
-
end
|
7
|
+
ActionView::Base.send :include, RPH::FormAssistant::ViewHelpers
|
@@ -0,0 +1,339 @@
|
|
1
|
+
# Developed by Ryan Heath (http://rpheath.com)
|
2
|
+
module RPH
|
3
|
+
# The idea is to make forms extremely less painful and a lot more DRY
|
4
|
+
module FormAssistant
|
5
|
+
FORM_HELPERS = [
|
6
|
+
ActionView::Helpers::FormBuilder.field_helpers +
|
7
|
+
%w(date_select datetime_select time_select collection_select select country_select time_zone_select) -
|
8
|
+
%w(hidden_field label fields_for)
|
9
|
+
].flatten.freeze
|
10
|
+
|
11
|
+
# FormAssistant::FormBuilder
|
12
|
+
# * provides several convenient helpers (see helpers.rb) and
|
13
|
+
# an infrastructure to easily add your own
|
14
|
+
# * method_missing hook to wrap content "on the fly"
|
15
|
+
# * optional: automatically attach labels to field helpers
|
16
|
+
# * optional: format fields using partials (extremely extensible)
|
17
|
+
#
|
18
|
+
# Usage:
|
19
|
+
#
|
20
|
+
# <% form_for @project, :builder => RPH::FormAssistant::FormBuilder do |form| %>
|
21
|
+
# // fancy form stuff
|
22
|
+
# <% end %>
|
23
|
+
#
|
24
|
+
# - or -
|
25
|
+
#
|
26
|
+
# <% form_assistant_for @project do |form| %>
|
27
|
+
# // fancy form stuff
|
28
|
+
# <% end %>
|
29
|
+
#
|
30
|
+
# - or -
|
31
|
+
#
|
32
|
+
# # in config/intializers/form_assistant.rb
|
33
|
+
# ActionView::Base.default_form_builder = RPH::FormAssistant::FormBuilder
|
34
|
+
class FormBuilder < ActionView::Helpers::FormBuilder
|
35
|
+
# used if no other template is available
|
36
|
+
attr_accessor :fallback_template
|
37
|
+
|
38
|
+
# override the field_error_proc so that it no longer wraps the field
|
39
|
+
# with <div class="fieldWithErrors">...</div>, but just returns the field
|
40
|
+
ActionView::Base.field_error_proc = Proc.new { |html_tag, instance| html_tag }
|
41
|
+
|
42
|
+
class << self
|
43
|
+
attr_accessor :ignore_templates
|
44
|
+
attr_accessor :ignore_labels
|
45
|
+
attr_accessor :ignore_errors
|
46
|
+
attr_accessor :template_root
|
47
|
+
|
48
|
+
# if set to true, none of the templates will be used;
|
49
|
+
# however, labels can still be automatically attached
|
50
|
+
# and all FormAssistant helpers are still avaialable
|
51
|
+
@ignore_templates = false
|
52
|
+
|
53
|
+
# if set to true, labels will become nil everywhere (both
|
54
|
+
# with and without templates)
|
55
|
+
@ignore_labels = false
|
56
|
+
|
57
|
+
# set to true if you'd rather use #error_messages_for()
|
58
|
+
@ignore_errors = false
|
59
|
+
|
60
|
+
def view_path
|
61
|
+
if Rails.configuration.respond_to?(:view_path)
|
62
|
+
return Rails.configuration.view_path
|
63
|
+
else
|
64
|
+
return Rails.configuration.paths['app/views'].first
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# sets the root directory where templates will be searched
|
69
|
+
# note: the template root should be nested within the
|
70
|
+
# configured view path (which defaults to app/views)
|
71
|
+
def template_root(full_path = false)
|
72
|
+
@template_root ||= File.join(view_path, 'forms')
|
73
|
+
|
74
|
+
# render(:partial => '...') doesn't want the full path of the template
|
75
|
+
full_path ? @template_root : @template_root.gsub(view_path, '')
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
# get the error messages (if any) for a field
|
81
|
+
def error_message_for(fields)
|
82
|
+
errors = []
|
83
|
+
fields = [fields] unless Array === fields
|
84
|
+
|
85
|
+
fields.each do |field|
|
86
|
+
next unless has_errors?(field)
|
87
|
+
|
88
|
+
errors += if RPH::FormAssistant::Rules.has_I18n_support?
|
89
|
+
full_messages_for(field)
|
90
|
+
else
|
91
|
+
human_field_name = field.to_s.humanize
|
92
|
+
errors += [*object.errors[field]].map do |error|
|
93
|
+
"#{human_field_name} #{error}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
errors.empty? ? nil : RPH::FormAssistant::FieldErrors.new(errors)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Returns full error messages for given field (uses I18n)
|
102
|
+
def full_messages_for(field)
|
103
|
+
attr_name = object.class.human_attribute_name(field.to_s)
|
104
|
+
|
105
|
+
object.errors[field].inject([]) do |full_messages, message|
|
106
|
+
next unless message
|
107
|
+
full_messages << attr_name + I18n.t('activerecord.errors.format.separator', :default => ' ') + message
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# returns true if a field is invalid
|
112
|
+
def has_errors?(field)
|
113
|
+
!(object.nil? || object.errors[field].blank?)
|
114
|
+
end
|
115
|
+
|
116
|
+
# checks to make sure the template exists
|
117
|
+
def template_exists?(template)
|
118
|
+
File.exists?(File.join(self.class.template_root(true), "_#{template}.html.erb"))
|
119
|
+
end
|
120
|
+
|
121
|
+
protected
|
122
|
+
# renders the appropriate partial located in the template root
|
123
|
+
def render_partial_for(element, field, label, tip, template, helper, required, extra_locals, args)
|
124
|
+
errors = self.class.ignore_errors ? nil : error_message_for(field)
|
125
|
+
locals = (extra_locals || {}).merge(:element => element, :field => field, :builder => self, :object => object, :object_name => object_name, :label => label, :errors => errors, :tip => tip, :helper => helper, :required => required)
|
126
|
+
|
127
|
+
@template.render :partial => "#{self.class.template_root}/#{template}.html.erb", :locals => locals
|
128
|
+
end
|
129
|
+
|
130
|
+
# render the element with an optional label (does not use the templates)
|
131
|
+
def render_element(element, field, name, options, ignore_label = false)
|
132
|
+
return element if ignore_label
|
133
|
+
|
134
|
+
# need to consider if the shortcut label option was used
|
135
|
+
# i.e. <%= form.text_field :title, :label => 'Project Title' %>
|
136
|
+
text, label_options = if options[:label].is_a?(String)
|
137
|
+
[options.delete(:label), {}]
|
138
|
+
else
|
139
|
+
[options[:label].delete(:text), options.delete(:label)]
|
140
|
+
end
|
141
|
+
|
142
|
+
# consider trailing labels
|
143
|
+
if %w(check_box radio_button).include?(name)
|
144
|
+
label_options[:class] = (label_options[:class].to_s + ' inline').strip
|
145
|
+
element + label(field, text, label_options)
|
146
|
+
else
|
147
|
+
label(field, text, label_options) + element
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def extract_options_for_label(field, options={})
|
152
|
+
label_options = {}
|
153
|
+
|
154
|
+
# consider the global setting for labels and
|
155
|
+
# allow for turning labels off on a per-helper basis
|
156
|
+
# <%= form.text_field :title, :label => false %>
|
157
|
+
if self.class.ignore_labels || options[:label] === false || field.blank?
|
158
|
+
label_options[:label] = false
|
159
|
+
else
|
160
|
+
# ensure that the :label option is a Hash from this point on
|
161
|
+
options[:label] ||= {}
|
162
|
+
|
163
|
+
# allow for a cleaner way of setting label text
|
164
|
+
# <%= form.text_field :whatever, :label => 'Whatever Title' %>
|
165
|
+
label_options.merge!(options[:label].is_a?(String) ? {:text => options[:label]} : options[:label])
|
166
|
+
|
167
|
+
# allow for a more convenient way to set common label options
|
168
|
+
# <%= form.text_field :whatever, :label_id => 'dom_id' %>
|
169
|
+
# <%= form.text_field :whatever, :label_class => 'required' %>
|
170
|
+
# <%= form.text_field :whatever, :label_text => 'Whatever' %>
|
171
|
+
%w(id class text).each do |option|
|
172
|
+
label_option = "label_#{option}".to_sym
|
173
|
+
label_options.merge!(option.to_sym => options.delete(label_option)) if options[label_option]
|
174
|
+
end
|
175
|
+
|
176
|
+
# Ensure we have default label text
|
177
|
+
# (since Rails' label() does not currently respect I18n)
|
178
|
+
label_options[:text] ||= object.class.human_attribute_name(field.to_s)
|
179
|
+
end
|
180
|
+
|
181
|
+
label_options
|
182
|
+
end
|
183
|
+
|
184
|
+
def extract_options_for_template(helper_name, options={})
|
185
|
+
template_options = {}
|
186
|
+
|
187
|
+
if options.has_key?(:template) && options[:template].kind_of?(FalseClass)
|
188
|
+
template_options[:template] = false
|
189
|
+
else
|
190
|
+
# grab the template
|
191
|
+
template = options.delete(:template) || helper_name.to_s
|
192
|
+
template = self.fallback_template unless template_exists?(template)
|
193
|
+
template_options[:template] = template
|
194
|
+
end
|
195
|
+
|
196
|
+
template_options
|
197
|
+
end
|
198
|
+
|
199
|
+
public
|
200
|
+
def fallback_template
|
201
|
+
@fallback_template ||= 'field'
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.assist(helper_name)
|
205
|
+
define_method(helper_name) do |field, *args|
|
206
|
+
options = (helper_name == 'check_box' ? args.shift : args.extract_options!) || {}
|
207
|
+
label_options = extract_options_for_label(field, options)
|
208
|
+
template_options = extract_options_for_template(helper_name, options)
|
209
|
+
extra_locals = options.delete(:locals) || {}
|
210
|
+
|
211
|
+
# build out the label element (if desired)
|
212
|
+
label = label_options[:label] === false ? nil : self.label(field, label_options.delete(:text), label_options)
|
213
|
+
|
214
|
+
# grab the tip, if any
|
215
|
+
tip = options.delete(:tip)
|
216
|
+
|
217
|
+
# is the field required?
|
218
|
+
required = !!options.delete(:required)
|
219
|
+
|
220
|
+
# ensure that we don't have any custom options pass through
|
221
|
+
field_options = options.except(:label, :template, :tip, :required)
|
222
|
+
|
223
|
+
# call the original render for the element
|
224
|
+
super_args = helper_name == 'check_box' ? args.unshift(field_options) : args.push(field_options)
|
225
|
+
element = super(field, *super_args)
|
226
|
+
|
227
|
+
return element if template_options[:template] === false
|
228
|
+
|
229
|
+
# return the helper with an optional label if templates are not to be used
|
230
|
+
return render_element(element, field, helper_name, options, label_options[:label] === false) if self.class.ignore_templates
|
231
|
+
|
232
|
+
# render the partial template from the desired template root
|
233
|
+
render_partial_for(element, field, label, tip, template_options[:template], helper_name, required, extra_locals, args)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# redefining all traditional form helpers so that they
|
238
|
+
# behave the way FormAssistant thinks they should behave
|
239
|
+
RPH::FormAssistant::FORM_HELPERS.each do |helper_name|
|
240
|
+
assist(helper_name)
|
241
|
+
end
|
242
|
+
|
243
|
+
def without_assistance(options={}, &block)
|
244
|
+
# TODO - allow options to only turn off templates and/or labels
|
245
|
+
ignore_labels, ignore_templates = self.class.ignore_labels, self.class.ignore_templates
|
246
|
+
|
247
|
+
begin
|
248
|
+
self.class.ignore_labels, self.class.ignore_templates = true, true
|
249
|
+
result = yield
|
250
|
+
ensure
|
251
|
+
self.class.ignore_labels, self.class.ignore_templates = ignore_labels, ignore_templates
|
252
|
+
end
|
253
|
+
|
254
|
+
result
|
255
|
+
end
|
256
|
+
|
257
|
+
def widget(*args, &block)
|
258
|
+
options = args.extract_options!
|
259
|
+
fields = args.shift || nil
|
260
|
+
field = Array === fields ? fields.first : fields
|
261
|
+
|
262
|
+
label_options = extract_options_for_label(field, options)
|
263
|
+
template_options = extract_options_for_template(self.fallback_template, options)
|
264
|
+
label = label_options[:label] === false ? nil : self.label(field, label_options.delete(:text), label_options)
|
265
|
+
tip = options.delete(:tip)
|
266
|
+
locals = options.delete(:locals)
|
267
|
+
required = !!options.delete(:required)
|
268
|
+
|
269
|
+
if block_given?
|
270
|
+
element = without_assistance do
|
271
|
+
@template.capture(&block)
|
272
|
+
end
|
273
|
+
else
|
274
|
+
element = nil
|
275
|
+
end
|
276
|
+
|
277
|
+
partial = render_partial_for(element, fields, label, tip, template_options[:template], 'widget', required, locals, args)
|
278
|
+
RPH::FormAssistant::Rules.binding_required? ? @template.concat(partial, block.binding) : @template.concat(partial)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Renders a partial, passing the form object as a local
|
282
|
+
# variable named 'form'
|
283
|
+
# <%= form.partial 'shared/new', :locals => { :whatever => @whatever } %>
|
284
|
+
def partial(name, options={})
|
285
|
+
(options[:locals] ||= {}).update :form => self
|
286
|
+
options.update :partial => name
|
287
|
+
@template.render options
|
288
|
+
end
|
289
|
+
|
290
|
+
def input(field, *args)
|
291
|
+
helper_name = case column_type(field)
|
292
|
+
when :string
|
293
|
+
field.to_s.include?('password') ? :password_field : :text_field
|
294
|
+
when :text ; :text_area
|
295
|
+
when :integer, :float, :decimal ; :text_field
|
296
|
+
when :date ; :date_select
|
297
|
+
when :datetime, :timestamp ; :datetime_select
|
298
|
+
when :time ; :time_select
|
299
|
+
when :boolean ; :check_box
|
300
|
+
else ; :text_field
|
301
|
+
end
|
302
|
+
|
303
|
+
send(helper_name, field, *args)
|
304
|
+
end
|
305
|
+
|
306
|
+
def inputs(*args)
|
307
|
+
options = args.extract_options!
|
308
|
+
args.flatten.map do |field|
|
309
|
+
input(field, options.dup)
|
310
|
+
end.join('')
|
311
|
+
end
|
312
|
+
|
313
|
+
def column_type(field)
|
314
|
+
object.class.columns_hash[field.to_s].type rescue :string
|
315
|
+
end
|
316
|
+
|
317
|
+
# since #fields_for() doesn't inherit the builder from form_for, we need
|
318
|
+
# to provide a means to set the builder automatically (works with nesting, too)
|
319
|
+
#
|
320
|
+
# Usage: simply call #fields_for() on the builder object
|
321
|
+
#
|
322
|
+
# <% form_assistant_for @project do |form| %>
|
323
|
+
# <%= form.text_field :title %>
|
324
|
+
# <% form.fields_for :tasks do |task_fields| %>
|
325
|
+
# <%= task_fields.text_field :name %>
|
326
|
+
# <% end %>
|
327
|
+
# <% end %>
|
328
|
+
def fields_for_with_form_assistant(record_or_name_or_array, *args, &proc)
|
329
|
+
options = args.extract_options!
|
330
|
+
# hand control over to the original #fields_for()
|
331
|
+
fields_for_without_form_assistant(record_or_name_or_array, *(args << options.merge!(:builder => self.class)), &proc)
|
332
|
+
end
|
333
|
+
|
334
|
+
# used to intercept #fields_for() and set the builder
|
335
|
+
alias_method_chain :fields_for, :form_assistant
|
336
|
+
end
|
337
|
+
|
338
|
+
end
|
339
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RPH
|
2
|
+
module FormAssistant
|
3
|
+
# methods that mix into ActionView::Base
|
4
|
+
module ViewHelpers
|
5
|
+
private
|
6
|
+
# used to ensure that the desired builder gets set before calling #form_for()
|
7
|
+
def form_for_with_builder(record_or_name_or_array, builder, *args, &proc)
|
8
|
+
options = args.extract_options!
|
9
|
+
# hand control over to the original #form_for()
|
10
|
+
form_for(record_or_name_or_array, *(args << options.merge!(:builder => builder)), &proc)
|
11
|
+
end
|
12
|
+
|
13
|
+
# determines if binding is needed for #concat()
|
14
|
+
# (Rails 2.2.0 and greater no longer requires the binding)
|
15
|
+
def binding_required
|
16
|
+
RPH::FormAssistant::Rules.binding_required?
|
17
|
+
end
|
18
|
+
|
19
|
+
public
|
20
|
+
# easy way to make use of FormAssistant::FormBuilder
|
21
|
+
#
|
22
|
+
# <% form_assistant_for @project do |form| %>
|
23
|
+
# // fancy form stuff
|
24
|
+
# <% end %>
|
25
|
+
def form_assistant_for(record_or_name_or_array, *args, &proc)
|
26
|
+
form_for_with_builder(record_or_name_or_array, RPH::FormAssistant::FormBuilder, *args, &proc)
|
27
|
+
end
|
28
|
+
|
29
|
+
# (borrowed the #fieldset() helper from Chris Scharf:
|
30
|
+
# http://github.com/scharfie/slate/tree/master/app/helpers/application_helper.rb)
|
31
|
+
#
|
32
|
+
# <% fieldset 'User Registration' do %>
|
33
|
+
# // fields
|
34
|
+
# <% end %>
|
35
|
+
def fieldset(legend, &block)
|
36
|
+
locals = { :legend => legend, :fields => capture(&block) }
|
37
|
+
partial = render(:partial => "#{RPH::FormAssistant::FormBuilder.template_root}/fieldset.html.erb", :locals => locals)
|
38
|
+
|
39
|
+
# render the fields
|
40
|
+
binding_required ? concat(partial, block.binding) : concat(partial)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/test/test_form_assistant.rb
CHANGED
@@ -67,7 +67,7 @@ end
|
|
67
67
|
|
68
68
|
class FormAssistantTest < ActionView::TestCase
|
69
69
|
include FormAssistantHelpers
|
70
|
-
include ::RPH::FormAssistant::
|
70
|
+
include ::RPH::FormAssistant::ViewHelpers
|
71
71
|
attr_accessor :form
|
72
72
|
|
73
73
|
def setup
|
@@ -77,6 +77,10 @@ class FormAssistantTest < ActionView::TestCase
|
|
77
77
|
@form = RPH::FormAssistant::FormBuilder.new(:address_book, @address_book, self, {}, nil)
|
78
78
|
RPH::FormAssistant::FormBuilder.template_root = File.expand_path(File.join(File.dirname(__FILE__), 'forms'))
|
79
79
|
end
|
80
|
+
|
81
|
+
test "should add helper methods" do
|
82
|
+
assert view.respond_to?(:form_assistant_for)
|
83
|
+
end
|
80
84
|
|
81
85
|
test "should use template based on input type" do
|
82
86
|
form.text_field :first_name
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: form_assistant
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 31
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
-
-
|
8
|
+
- 2
|
9
9
|
- 0
|
10
|
-
version: 1.
|
10
|
+
version: 1.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ryan Heath
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2012-07-
|
19
|
+
date: 2012-07-07 00:00:00 -04:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
@@ -182,12 +182,12 @@ files:
|
|
182
182
|
- forms/_radio_button.html.erb
|
183
183
|
- forms/_text_area.html.erb
|
184
184
|
- forms/_text_field.html.erb
|
185
|
-
- init.rb
|
186
|
-
- install.rb
|
187
185
|
- lib/form_assistant.rb
|
188
186
|
- lib/form_assistant/error.rb
|
189
187
|
- lib/form_assistant/field_errors.rb
|
188
|
+
- lib/form_assistant/form_builder.rb
|
190
189
|
- lib/form_assistant/rules.rb
|
190
|
+
- lib/form_assistant/view_helpers.rb
|
191
191
|
- tasks/form_assistant_tasks.rake
|
192
192
|
- test/forms/_field.html.erb
|
193
193
|
- test/forms/_fieldset.html.erb
|
data/init.rb
DELETED
data/install.rb
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
# Install hook code here
|