merb_helpers 0.9.4 → 0.9.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|