merb_helpers 0.9.4 → 0.9.5
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/Rakefile +2 -2
- data/lib/merb_helpers/core_ext.rb +2 -0
- data/lib/merb_helpers/form/builder.rb +394 -0
- data/lib/merb_helpers/form/helpers.rb +410 -0
- data/lib/merb_helpers/form_helpers.rb +10 -651
- metadata +6 -3
data/Rakefile
CHANGED
@@ -18,7 +18,7 @@ GEM_EMAIL = "ykatz@engineyard.com"
|
|
18
18
|
|
19
19
|
GEM_NAME = "merb_helpers"
|
20
20
|
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
21
|
-
GEM_VERSION = (Merb::MORE_VERSION rescue "0.9.
|
21
|
+
GEM_VERSION = (Merb::MORE_VERSION rescue "0.9.5") + PKG_BUILD
|
22
22
|
|
23
23
|
RELEASE_NAME = "REL #{GEM_VERSION}"
|
24
24
|
|
@@ -36,7 +36,7 @@ spec = Gem::Specification.new do |s|
|
|
36
36
|
s.author = GEM_AUTHOR
|
37
37
|
s.email = GEM_EMAIL
|
38
38
|
s.homepage = PROJECT_URL
|
39
|
-
s.add_dependency('merb-core', '>= 0.9.
|
39
|
+
s.add_dependency('merb-core', '>= 0.9.5')
|
40
40
|
s.require_path = 'lib'
|
41
41
|
s.files = %w(LICENSE README Rakefile TODO) + Dir.glob("{lib,specs}/**/*")
|
42
42
|
end
|
@@ -0,0 +1,394 @@
|
|
1
|
+
load File.dirname(__FILE__) / ".." / "tag_helpers.rb"
|
2
|
+
|
3
|
+
module Merb::Helpers::Form::Builder
|
4
|
+
|
5
|
+
class Base
|
6
|
+
include Merb::Helpers::Tag
|
7
|
+
|
8
|
+
def initialize(obj, name, origin)
|
9
|
+
@obj, @origin = obj, origin
|
10
|
+
@name = name || @obj.class.name.snake_case.split("/").last
|
11
|
+
end
|
12
|
+
|
13
|
+
def concat(attrs, &blk)
|
14
|
+
@origin.concat(@origin.capture(&blk), blk.binding)
|
15
|
+
end
|
16
|
+
|
17
|
+
def fieldset(attrs, &blk)
|
18
|
+
legend = (l_attr = attrs.delete(:legend)) ? tag(:legend, l_attr) : ""
|
19
|
+
tag(:fieldset, legend + @origin.capture(&blk), attrs)
|
20
|
+
# @origin.concat(contents, blk.binding)
|
21
|
+
end
|
22
|
+
|
23
|
+
def form(attrs = {}, &blk)
|
24
|
+
captured = @origin.capture(&blk)
|
25
|
+
fake_method_tag = process_form_attrs(attrs)
|
26
|
+
|
27
|
+
tag(:form, fake_method_tag + captured, attrs)
|
28
|
+
end
|
29
|
+
|
30
|
+
def process_form_attrs(attrs)
|
31
|
+
method = attrs[:method]
|
32
|
+
|
33
|
+
# Unless the method is :get, fake out the method using :post
|
34
|
+
attrs[:method] = :post unless attrs[:method] == :get
|
35
|
+
# Use a fake PUT if the object is not new, otherwise use the method
|
36
|
+
# passed in.
|
37
|
+
method ||= (@obj && !@obj.new_record? ? :put : :post)
|
38
|
+
|
39
|
+
attrs[:enctype] = "multipart/form-data" if attrs.delete(:multipart) || @multipart
|
40
|
+
|
41
|
+
method == :post || method == :get ? "" : fake_out_method(attrs, method)
|
42
|
+
end
|
43
|
+
|
44
|
+
# This can be overridden to use another method to fake out methods
|
45
|
+
def fake_out_method(attrs, method)
|
46
|
+
self_closing_tag(:input, :type => "hidden", :name => "_method", :value => method)
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_css_class(attrs, new_class)
|
50
|
+
attrs[:class] = attrs[:class] ? "#{attrs[:class]} #{new_class}" : new_class
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_bound_controls(method, attrs, type)
|
54
|
+
case type
|
55
|
+
when "checkbox"
|
56
|
+
update_bound_check_box(method, attrs)
|
57
|
+
when "select"
|
58
|
+
update_bound_select(method, attrs)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_bound_select(method, attrs)
|
63
|
+
attrs[:value_method] ||= method
|
64
|
+
attrs[:text_method] ||= attrs[:value_method] || :to_s
|
65
|
+
attrs[:selected] ||= @obj.send(attrs[:value_method])
|
66
|
+
end
|
67
|
+
|
68
|
+
def update_bound_check_box(method, attrs)
|
69
|
+
raise ArgumentError, ":value can't be used with a bound_check_box" if attrs.has_key?(:value)
|
70
|
+
|
71
|
+
attrs[:boolean] ||= true
|
72
|
+
|
73
|
+
val = @obj.send(method)
|
74
|
+
attrs[:checked] = attrs.key?(:on) ? val == attrs[:on] : considered_true?(val)
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_unbound_controls(attrs, type)
|
78
|
+
case type
|
79
|
+
when "checkbox"
|
80
|
+
update_unbound_check_box(attrs)
|
81
|
+
when "file"
|
82
|
+
@multipart = true
|
83
|
+
end
|
84
|
+
|
85
|
+
attrs[:disabled] ? attrs[:disabled] = "disabled" : attrs.delete(:disabled)
|
86
|
+
end
|
87
|
+
|
88
|
+
def update_unbound_check_box(attrs)
|
89
|
+
boolean = attrs[:boolean] || (attrs[:on] && attrs[:off]) ? true : false
|
90
|
+
|
91
|
+
case
|
92
|
+
when attrs.key?(:on) ^ attrs.key?(:off)
|
93
|
+
raise ArgumentError, ":on and :off must be specified together"
|
94
|
+
when (attrs[:boolean] == false) && (attrs.key?(:on))
|
95
|
+
raise ArgumentError, ":boolean => false cannot be used with :on and :off"
|
96
|
+
when boolean && attrs.key?(:value)
|
97
|
+
raise ArgumentError, ":value can't be used with a boolean checkbox"
|
98
|
+
end
|
99
|
+
|
100
|
+
if attrs[:boolean] = boolean
|
101
|
+
attrs[:on] ||= "1"; attrs[:off] ||= "0"
|
102
|
+
end
|
103
|
+
|
104
|
+
attrs[:checked] = "checked" if attrs.delete(:checked)
|
105
|
+
end
|
106
|
+
|
107
|
+
def bound_check_box(method, attrs = {})
|
108
|
+
name = control_name(method)
|
109
|
+
update_bound_controls(method, attrs, "checkbox")
|
110
|
+
unbound_check_box({:name => name}.merge(attrs))
|
111
|
+
end
|
112
|
+
|
113
|
+
def unbound_check_box(attrs)
|
114
|
+
update_unbound_controls(attrs, "checkbox")
|
115
|
+
if attrs.delete(:boolean)
|
116
|
+
on, off = attrs.delete(:on), attrs.delete(:off)
|
117
|
+
unbound_hidden_field(:name => attrs[:name], :value => off) <<
|
118
|
+
self_closing_tag(:input, {:type => "checkbox", :value => on}.merge(attrs))
|
119
|
+
else
|
120
|
+
self_closing_tag(:input, {:type => "checkbox"}.merge(attrs))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
%w(text password hidden file).each do |kind|
|
125
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
126
|
+
def unbound_#{kind}_field(attrs)
|
127
|
+
update_unbound_controls(attrs, "#{kind}")
|
128
|
+
self_closing_tag(:input, {:type => "#{kind}"}.merge(attrs))
|
129
|
+
end
|
130
|
+
|
131
|
+
def bound_#{kind}_field(method, attrs = {})
|
132
|
+
name = control_name(method)
|
133
|
+
update_bound_controls(method, attrs, "#{kind}")
|
134
|
+
unbound_#{kind}_field({:name => name, :value => @obj.send(method)}.merge(attrs))
|
135
|
+
end
|
136
|
+
RUBY
|
137
|
+
end
|
138
|
+
|
139
|
+
def bound_radio_button(method, attrs = {})
|
140
|
+
name = control_name(method)
|
141
|
+
update_bound_controls(method, attrs, "radio")
|
142
|
+
unbound_radio_button({:name => name, :value => @obj.send(method)}.merge(attrs))
|
143
|
+
end
|
144
|
+
|
145
|
+
def unbound_radio_button(attrs)
|
146
|
+
update_unbound_controls(attrs, "radio")
|
147
|
+
self_closing_tag(:input, {:type => "radio"}.merge(attrs))
|
148
|
+
end
|
149
|
+
|
150
|
+
def button(contents, attrs)
|
151
|
+
update_unbound_controls(attrs, "button")
|
152
|
+
tag(:button, contents, attrs)
|
153
|
+
end
|
154
|
+
|
155
|
+
def submit(value, attrs)
|
156
|
+
attrs[:type] ||= "submit"
|
157
|
+
attrs[:name] ||= "submit"
|
158
|
+
attrs[:value] ||= value
|
159
|
+
update_unbound_controls(attrs, "submit")
|
160
|
+
self_closing_tag(:input, attrs)
|
161
|
+
end
|
162
|
+
|
163
|
+
def bound_select(method, attrs = {})
|
164
|
+
name = control_name(method)
|
165
|
+
update_bound_controls(method, attrs, "select")
|
166
|
+
unbound_select({:name => name}.merge(attrs))
|
167
|
+
end
|
168
|
+
|
169
|
+
def unbound_select(attrs = {})
|
170
|
+
update_unbound_controls(attrs, "select")
|
171
|
+
tag(:select, options_for(attrs), attrs)
|
172
|
+
end
|
173
|
+
|
174
|
+
def bound_radio_group(method, arr)
|
175
|
+
val = @obj.send(method)
|
176
|
+
arr.map do |attrs|
|
177
|
+
attrs = {:value => attrs} unless attrs.is_a?(Hash)
|
178
|
+
attrs[:checked] ||= (val == attrs[:value])
|
179
|
+
radio_group_item(method, attrs)
|
180
|
+
end.join
|
181
|
+
end
|
182
|
+
|
183
|
+
def unbound_text_area(contents, attrs)
|
184
|
+
update_unbound_controls(attrs, "text_area")
|
185
|
+
tag(:textarea, contents, attrs)
|
186
|
+
end
|
187
|
+
|
188
|
+
def bound_text_area(method, attrs = {})
|
189
|
+
name = "#{@name}[#{method}]"
|
190
|
+
update_bound_controls(method, attrs, "text_area")
|
191
|
+
unbound_text_area(@obj.send(method), {:name => name}.merge(attrs))
|
192
|
+
end
|
193
|
+
|
194
|
+
private
|
195
|
+
|
196
|
+
def control_name(method)
|
197
|
+
"#{@name}[#{method}]"
|
198
|
+
end
|
199
|
+
|
200
|
+
# Accepts a collection (hash, array, enumerable, your type) and returns a string of option tags.
|
201
|
+
# Given a collection where the elements respond to first and last (such as a two-element array),
|
202
|
+
# the "lasts" serve as option values and the "firsts" as option text. Hashes are turned into
|
203
|
+
# this form automatically, so the keys become "firsts" and values become lasts. If selected is
|
204
|
+
# specified, the matching "last" or element will get the selected option-tag. Selected may also
|
205
|
+
# be an array of values to be selected when using a multiple select.
|
206
|
+
#
|
207
|
+
# ==== Parameters
|
208
|
+
# attrs<Hash>:: HTML attributes and options
|
209
|
+
#
|
210
|
+
# ==== Options
|
211
|
+
# +selected+:: The value of a selected object, which may be either a string or an array.
|
212
|
+
# +prompt+:: Adds an addtional option tag with the provided string with no value.
|
213
|
+
# +include_blank+:: Adds an additional blank option tag with no value.
|
214
|
+
#
|
215
|
+
# ==== Returns
|
216
|
+
# String:: HTML
|
217
|
+
#
|
218
|
+
# ==== Examples
|
219
|
+
# <%= options_for [["apple", "Apple Pie"], ["orange", "Orange Juice"]], :selected => "orange"
|
220
|
+
# => <option value="apple">Apple Pie</option><option value="orange" selected="selected">Orange Juice</option>
|
221
|
+
#
|
222
|
+
# <%= options_for [["apple", "Apple Pie"], ["orange", "Orange Juice"]], :selected => ["orange", "apple"], :prompt => "Select One"
|
223
|
+
# => <option value="">Select One</option><option value="apple" selected="selected">Apple Pie</option><option value="orange" selected="selected">Orange Juice</option>
|
224
|
+
def options_for(attrs)
|
225
|
+
blank, prompt = attrs.delete(:include_blank), attrs.delete(:prompt)
|
226
|
+
b = blank || prompt ? tag(:option, prompt || "", :value => "") : ""
|
227
|
+
|
228
|
+
# yank out the options attrs
|
229
|
+
collection, selected, text_method, value_method =
|
230
|
+
attrs.extract!(:collection, :selected, :text_method, :value_method)
|
231
|
+
|
232
|
+
# if the collection is a Hash, optgroups are a-coming
|
233
|
+
if collection.is_a?(Hash)
|
234
|
+
([b] + collection.map do |g,col|
|
235
|
+
tag(:optgroup, options(col, text_method, value_method, selected), :label => g)
|
236
|
+
end).join
|
237
|
+
else
|
238
|
+
options(collection || [], text_method, value_method, selected, b)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def options(col, text_meth, value_meth, sel, b = nil)
|
243
|
+
([b] + col.map do |item|
|
244
|
+
text_meth = text_meth && item.respond_to?(text_meth) ? text_meth : :last
|
245
|
+
value_meth = value_meth && item.respond_to?(value_meth) ? value_meth : :first
|
246
|
+
|
247
|
+
text = item.is_a?(String) ? item : item.send(text_meth)
|
248
|
+
value = item.is_a?(String) ? item : item.send(value_meth)
|
249
|
+
|
250
|
+
option_attrs = {:value => value}
|
251
|
+
option_attrs.merge!(:selected => "selected") if value == sel
|
252
|
+
tag(:option, text, option_attrs)
|
253
|
+
end).join
|
254
|
+
end
|
255
|
+
|
256
|
+
def radio_group_item(method, attrs)
|
257
|
+
attrs.merge!(:checked => "checked") if attrs[:checked]
|
258
|
+
bound_radio_button(method, attrs)
|
259
|
+
end
|
260
|
+
|
261
|
+
def considered_true?(value)
|
262
|
+
value && value != "0" && value != 0
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
class Form < Base
|
267
|
+
def update_bound_controls(method, attrs, type)
|
268
|
+
attrs.merge!(:id => "#{@name}_#{method}") unless attrs[:id]
|
269
|
+
super
|
270
|
+
end
|
271
|
+
|
272
|
+
def update_unbound_controls(attrs, type)
|
273
|
+
case type
|
274
|
+
when "text", "radio", "password", "hidden", "checkbox", "file"
|
275
|
+
add_css_class(attrs, type)
|
276
|
+
end
|
277
|
+
super
|
278
|
+
end
|
279
|
+
|
280
|
+
# Provides a generic HTML label.
|
281
|
+
#
|
282
|
+
# ==== Parameters
|
283
|
+
# attrs<Hash>:: HTML attributes
|
284
|
+
#
|
285
|
+
# ==== Returns
|
286
|
+
# String:: HTML
|
287
|
+
#
|
288
|
+
# ==== Example
|
289
|
+
# <%= label :for => "name", :label => "Full Name" %>
|
290
|
+
# => <label for="name">Full Name</label>
|
291
|
+
def label(attrs)
|
292
|
+
attrs ||= {}
|
293
|
+
for_attr = attrs[:id] ? {:for => attrs[:id]} : {}
|
294
|
+
if label_text = attrs.delete(:label)
|
295
|
+
tag(:label, label_text, for_attr)
|
296
|
+
else
|
297
|
+
""
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
%w(text password file).each do |kind|
|
302
|
+
self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
303
|
+
def unbound_#{kind}_field(attrs = {})
|
304
|
+
label(attrs) + super
|
305
|
+
end
|
306
|
+
RUBY
|
307
|
+
end
|
308
|
+
|
309
|
+
def button(contents, attrs = {})
|
310
|
+
label(attrs) + super
|
311
|
+
end
|
312
|
+
|
313
|
+
def submit(value, attrs = {})
|
314
|
+
label(attrs) + super
|
315
|
+
end
|
316
|
+
|
317
|
+
def unbound_text_area(contents, attrs = {})
|
318
|
+
label(attrs) + super
|
319
|
+
end
|
320
|
+
|
321
|
+
def unbound_select(attrs = {})
|
322
|
+
label(attrs) + super
|
323
|
+
end
|
324
|
+
|
325
|
+
def unbound_check_box(attrs = {})
|
326
|
+
label_text = label(attrs)
|
327
|
+
super + label_text
|
328
|
+
end
|
329
|
+
|
330
|
+
def unbound_radio_button(attrs = {})
|
331
|
+
label_text = label(attrs)
|
332
|
+
super + label_text
|
333
|
+
end
|
334
|
+
|
335
|
+
def radio_group_item(method, attrs)
|
336
|
+
unless attrs[:id]
|
337
|
+
attrs.merge!(:id => "#{@name}_#{method}_#{attrs[:value]}")
|
338
|
+
end
|
339
|
+
|
340
|
+
attrs.merge!(:label => attrs[:label] || attrs[:value])
|
341
|
+
super
|
342
|
+
end
|
343
|
+
|
344
|
+
def unbound_hidden_field(attrs = {})
|
345
|
+
attrs.delete(:label)
|
346
|
+
super
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
module Errorifier
|
351
|
+
def update_bound_controls(method, attrs, type)
|
352
|
+
if @obj.errors.on(method.to_sym)
|
353
|
+
add_css_class(attrs, "error")
|
354
|
+
end
|
355
|
+
super
|
356
|
+
end
|
357
|
+
|
358
|
+
def error_messages_for(obj, error_class, build_li, header, before)
|
359
|
+
obj ||= @obj
|
360
|
+
return "" unless obj.respond_to?(:errors)
|
361
|
+
|
362
|
+
sequel = !obj.errors.respond_to?(:each)
|
363
|
+
errors = sequel ? obj.errors.full_messages : obj.errors
|
364
|
+
|
365
|
+
return "" if errors.empty?
|
366
|
+
|
367
|
+
header_message = header % [errors.size, errors.size == 1 ? "" : "s"]
|
368
|
+
markup = %Q{<div class='#{error_class}'>#{header_message}<ul>}
|
369
|
+
errors.each {|err| markup << (build_li % (sequel ? err : err.join(" ")))}
|
370
|
+
markup << %Q{</ul></div>}
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
class FormWithErrors < Form
|
375
|
+
include Errorifier
|
376
|
+
end
|
377
|
+
|
378
|
+
module Resourceful
|
379
|
+
def process_form_attrs(attrs)
|
380
|
+
attrs[:action] ||= url(@name, @obj) if @origin
|
381
|
+
super
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
class ResourcefulForm < Form
|
386
|
+
include Resourceful
|
387
|
+
end
|
388
|
+
|
389
|
+
class ResourcefulFormWithErrors < FormWithErrors
|
390
|
+
include Errorifier
|
391
|
+
include Resourceful
|
392
|
+
end
|
393
|
+
|
394
|
+
end
|