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 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.4") + PKG_BUILD
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.4')
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
@@ -1,3 +1,4 @@
1
+ require "date"
1
2
  class Date
2
3
  include OrdinalizedFormatting
3
4
 
@@ -28,4 +29,5 @@ class Time
28
29
  end
29
30
 
30
31
  def to_time; self; end
32
+ public :to_date
31
33
  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