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 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