padrino-helpers 0.10.7 → 0.11.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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/lib/padrino-helpers.rb +2 -1
  3. data/lib/padrino-helpers/asset_tag_helpers.rb +57 -65
  4. data/lib/padrino-helpers/breadcrumb_helpers.rb +171 -0
  5. data/lib/padrino-helpers/form_builder/abstract_form_builder.rb +82 -24
  6. data/lib/padrino-helpers/form_helpers.rb +84 -17
  7. data/lib/padrino-helpers/format_helpers.rb +2 -1
  8. data/lib/padrino-helpers/locale/fr.yml +12 -12
  9. data/lib/padrino-helpers/locale/pt_br.yml +2 -2
  10. data/lib/padrino-helpers/output_helpers.rb +42 -2
  11. data/lib/padrino-helpers/output_helpers/erb_handler.rb +1 -1
  12. data/lib/padrino-helpers/output_helpers/slim_handler.rb +4 -5
  13. data/lib/padrino-helpers/render_helpers.rb +2 -2
  14. data/lib/padrino-helpers/tag_helpers.rb +33 -5
  15. data/padrino-helpers.gemspec +0 -0
  16. data/test/fixtures/markup_app/app.rb +13 -6
  17. data/test/fixtures/markup_app/views/capture_concat.erb +2 -2
  18. data/test/fixtures/markup_app/views/capture_concat.haml +2 -2
  19. data/test/fixtures/markup_app/views/capture_concat.slim +4 -5
  20. data/test/fixtures/markup_app/views/content_for.slim +4 -4
  21. data/test/fixtures/markup_app/views/content_tag.slim +5 -5
  22. data/test/fixtures/markup_app/views/current_engine.haml +1 -1
  23. data/test/fixtures/markup_app/views/fields_for.slim +13 -13
  24. data/test/fixtures/markup_app/views/form_for.slim +43 -43
  25. data/test/fixtures/markup_app/views/form_tag.slim +57 -57
  26. data/test/fixtures/markup_app/views/link_to.slim +2 -2
  27. data/test/fixtures/markup_app/views/mail_to.slim +2 -2
  28. data/test/fixtures/markup_app/views/meta_tag.slim +2 -2
  29. data/test/fixtures/markup_app/views/partials/_slim.slim +1 -1
  30. data/test/fixtures/markup_app/views/simple_partial.slim +1 -1
  31. data/test/fixtures/render_app/app.rb +3 -0
  32. data/test/test_asset_tag_helpers.rb +17 -4
  33. data/test/test_form_builder.rb +35 -1
  34. data/test/test_form_helpers.rb +29 -0
  35. data/test/test_format_helpers.rb +4 -0
  36. data/test/test_output_helpers.rb +5 -3
  37. data/test/test_tag_helpers.rb +11 -0
  38. metadata +10 -15
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f362f51264ab447da505f5bbaeb295b467cff819
4
+ data.tar.gz: c0a4370011a89876d15d3668a82e3451e5b9b6cb
5
+ SHA512:
6
+ metadata.gz: c3b645ff7c6bbdafc844465cb6f78fcdea389ee27ccff8b6dabd2f7eb352ebb1f256f0cea9c67914a6d56ba74609c678a2703b7b7451558c73ef1822877a511f
7
+ data.tar.gz: fafd39ce16c2639cbde4537a6b6530b2580e1c59ae9c397298dd16d8416bc9a241e7eeff8c4a91f4548609f64339162ec28ad597398a1c6a93839f11c6706652
@@ -4,7 +4,6 @@ require 'i18n'
4
4
  require 'enumerator'
5
5
  require 'active_support/time_with_zone' # next extension depends on this
6
6
  require 'active_support/core_ext/string/conversions' # to_date
7
- require 'active_support/core_ext/float/rounding' # round
8
7
  require 'active_support/option_merger' # with_options
9
8
  require 'active_support/core_ext/object/with_options' # with_options
10
9
  require 'active_support/inflector' # humanize
@@ -32,6 +31,7 @@ module Padrino
32
31
  # Padrino::Helpers::FormatHelpers
33
32
  # Padrino::Helpers::RenderHelpers
34
33
  # Padrino::Helpers::NumberHelpers
34
+ # Padrino::Helpers::Breadcrumbs
35
35
  #
36
36
  # @param [Sinatra::Application] app
37
37
  # The specified padrino application
@@ -52,6 +52,7 @@ module Padrino
52
52
  app.helpers Padrino::Helpers::RenderHelpers
53
53
  app.helpers Padrino::Helpers::NumberHelpers
54
54
  app.helpers Padrino::Helpers::TranslationHelpers
55
+ app.helpers Padrino::Helpers::Breadcrumbs
55
56
  end
56
57
  alias :included :registered
57
58
  end
@@ -4,6 +4,10 @@ module Padrino
4
4
  # Helpers related to producing assets (images,stylesheets,js,etc) within templates.
5
5
  #
6
6
  module AssetTagHelpers
7
+ FRAGMENT_HASH = "#".html_safe.freeze
8
+ # assets that require an appended extension
9
+ APPEND_ASSET_EXTENSIONS = ["js", "css"]
10
+
7
11
  ##
8
12
  # Creates a div to display the flash of given type if it exists
9
13
  #
@@ -11,6 +15,7 @@ module Padrino
11
15
  # The type of flash to display in the tag.
12
16
  # @param [Hash] options
13
17
  # The html options for this section.
18
+ # use :bootstrap => true to support Twitter's bootstrap dismiss alert button
14
19
  #
15
20
  # @return [String] Flash tag html with specified +options+.
16
21
  #
@@ -24,11 +29,13 @@ module Padrino
24
29
  # @api public
25
30
  def flash_tag(*args)
26
31
  options = args.extract_options!
27
- args.map do |kind|
32
+ bootstrap = options.delete(:bootstrap) if options[:bootstrap]
33
+ args.inject(''.html_safe) do |html,kind|
28
34
  flash_text = flash[kind]
29
35
  next if flash_text.blank?
30
- content_tag(:div, flash_text, options.reverse_merge(:class => kind))
31
- end.compact * "\n"
36
+ flash_text << safe_content_tag(:button, "&times;", {:type => :button, :class => :close, :'data-dismiss' => :alert}) if bootstrap
37
+ html << safe_content_tag(:div, flash_text, options.reverse_merge(:class => kind))
38
+ end
32
39
  end
33
40
 
34
41
  ##
@@ -45,6 +52,8 @@ module Padrino
45
52
  #
46
53
  # @option options [String] :anchor
47
54
  # The anchor for the link (i.e #something)
55
+ # @option options [String] :fragment
56
+ # Synonym for anchor
48
57
  # @option options [Boolean] :if
49
58
  # If true, the link will appear, otherwise not;
50
59
  # @option options [Boolean] :unless
@@ -70,66 +79,38 @@ module Padrino
70
79
  # @api public
71
80
  def link_to(*args, &block)
72
81
  options = args.extract_options!
73
- anchor = "##{CGI.escape options.delete(:anchor).to_s}" if options[:anchor]
82
+ fragment = options.delete(:anchor).to_s if options[:anchor]
83
+ fragment = options.delete(:fragment).to_s if options[:fragment]
74
84
 
85
+ url = ActiveSupport::SafeBuffer.new
75
86
  if block_given?
76
- url = args[0] ? args[0] + anchor.to_s : anchor || '#'
87
+ if args[0]
88
+ url.concat(args[0])
89
+ url.concat(FRAGMENT_HASH).concat(fragment) if fragment
90
+ else
91
+ url.concat(FRAGMENT_HASH)
92
+ url.concat(fragment) if fragment
93
+ end
77
94
  options.reverse_merge!(:href => url)
78
95
  link_content = capture_html(&block)
79
96
  return '' unless parse_conditions(url, options)
80
97
  result_link = content_tag(:a, link_content, options)
81
98
  block_is_template?(block) ? concat_content(result_link) : result_link
82
99
  else
83
- name, url = args[0], (args[1] ? args[1] + anchor.to_s : anchor || '#')
100
+ if args[1]
101
+ url.concat(args[1])
102
+ url.safe_concat(FRAGMENT_HASH).concat(fragment) if fragment
103
+ else
104
+ url = FRAGMENT_HASH
105
+ url.concat(fragment) if fragment
106
+ end
107
+ name = args[0]
84
108
  return name unless parse_conditions(url, options)
85
109
  options.reverse_merge!(:href => url)
86
110
  content_tag(:a, name, options)
87
111
  end
88
112
  end
89
113
 
90
- ##
91
- # Creates a form containing a single button that submits to the url.
92
- #
93
- # @overload button_to(name, url, options={})
94
- # @param [String] caption The text caption.
95
- # @param [String] url The url href.
96
- # @param [Hash] options The html options.
97
- # @overload button_to(name, options={}, &block)
98
- # @param [String] url The url href.
99
- # @param [Hash] options The html options.
100
- # @param [Proc] block The button content.
101
- #
102
- # @option options [Boolean] :multipart
103
- # If true, this form will support multipart encoding.
104
- # @option options [String] :remote
105
- # Instructs ujs handler to handle the submit as ajax.
106
- # @option options [Symbol] :method
107
- # Instructs ujs handler to use different http method (i.e :post, :delete).
108
- #
109
- # @return [String] Form and button html with specified +options+.
110
- #
111
- # @example
112
- # button_to 'Delete', url(:accounts_destroy, :id => account), :method => :delete, :class => :form
113
- # # Generates:
114
- # # <form class="form" action="/admin/accounts/destroy/2" method="post">
115
- # # <input type="hidden" value="delete" name="_method" />
116
- # # <input type="submit" value="Delete" />
117
- # # </form>
118
- #
119
- # @api public
120
- def button_to(*args, &block)
121
- name, url = args[0], args[1]
122
- options = args.extract_options!
123
- desired_method = options[:method]
124
- options.delete(:method) if options[:method].to_s !~ /get|post/i
125
- options.reverse_merge!(:method => 'post', :action => url)
126
- options[:enctype] = 'multipart/form-data' if options.delete(:multipart)
127
- options['data-remote'] = 'true' if options.delete(:remote)
128
- inner_form_html = hidden_form_method_field(desired_method)
129
- inner_form_html += block_given? ? capture_html(&block) : submit_tag(name)
130
- content_tag('form', inner_form_html, options)
131
- end
132
-
133
114
  ##
134
115
  # Creates a link tag that browsers and news readers can use to auto-detect an RSS or ATOM feed.
135
116
  #
@@ -185,7 +166,7 @@ module Padrino
185
166
  # @api public
186
167
  def mail_to(email, caption=nil, mail_options={})
187
168
  html_options = mail_options.slice!(:cc, :bcc, :subject, :body)
188
- mail_query = Rack::Utils.build_query(mail_options).gsub(/\+/, '%20').gsub('%40', '@')
169
+ mail_query = Rack::Utils.build_query(mail_options).gsub(/\+/, '%20').gsub('%40', '@').gsub('&', '&amp;')
189
170
  mail_href = "mailto:#{email}"; mail_href << "?#{mail_query}" if mail_query.present?
190
171
  link_to((caption || email), mail_href, html_options)
191
172
  end
@@ -275,7 +256,7 @@ module Padrino
275
256
  options.reverse_merge!(:media => 'screen', :rel => 'stylesheet', :type => 'text/css')
276
257
  sources.flatten.map { |source|
277
258
  tag(:link, options.reverse_merge(:href => asset_path(:css, source)))
278
- }.join("\n")
259
+ }.join("\n").html_safe
279
260
  end
280
261
 
281
262
  ##
@@ -298,7 +279,7 @@ module Padrino
298
279
  options.reverse_merge!(:type => 'text/javascript')
299
280
  sources.flatten.map { |source|
300
281
  content_tag(:script, nil, options.reverse_merge(:src => asset_path(:js, source)))
301
- }.join("\n")
282
+ }.join("\n").html_safe
302
283
  end
303
284
 
304
285
  ##
@@ -341,14 +322,11 @@ module Padrino
341
322
  #
342
323
  # @api semipublic
343
324
  def asset_path(kind, source)
344
- return source if source =~ /^http/
345
- is_absolute = source =~ %r{^/}
346
- asset_folder = asset_folder_name(kind)
347
- source = source.to_s.gsub(/\s/, '%20')
348
- ignore_extension = (asset_folder.to_s == kind.to_s) # don't append an extension
349
- source << ".#{kind}" unless ignore_extension or source =~ /\.#{kind}/
350
- result_path = is_absolute ? source : uri_root_path(asset_folder, source)
351
- timestamp = asset_timestamp(result_path, is_absolute)
325
+ source = asset_normalize_extension(kind, URI.escape(source.to_s))
326
+ return source if source =~ %r{^(/|https?://)} # absolute source
327
+ source = File.join(asset_folder_name(kind), source)
328
+ timestamp = asset_timestamp(source)
329
+ result_path = uri_root_path(source)
352
330
  "#{result_path}#{timestamp}"
353
331
  end
354
332
 
@@ -370,14 +348,15 @@ module Padrino
370
348
  #
371
349
  # @example
372
350
  # asset_timestamp("some/path/to/file.png") => "?154543678"
373
- # asset_timestamp("/some/absolute/path.png", true) => nil
374
351
  #
375
- def asset_timestamp(file_path, absolute=false)
352
+ def asset_timestamp(file_path)
376
353
  return nil if file_path =~ /\?/ || (self.class.respond_to?(:asset_stamp) && !self.class.asset_stamp)
377
- public_file_path = Padrino.root("public", file_path) if Padrino.respond_to?(:root)
354
+ public_path = self.class.public_folder if self.class.respond_to?(:public_folder)
355
+ public_path ||= Padrino.root("public") if Padrino.respond_to?(:root)
356
+ public_file_path = File.join(public_path, file_path) if public_path
378
357
  stamp = File.mtime(public_file_path).to_i if public_file_path && File.exist?(public_file_path)
379
- stamp ||= Time.now.to_i unless absolute
380
- "?#{stamp}" if stamp
358
+ stamp ||= Time.now.to_i
359
+ "?#{stamp}"
381
360
  end
382
361
 
383
362
  ###
@@ -396,6 +375,19 @@ module Padrino
396
375
  end
397
376
  end
398
377
 
378
+ # Normalizes the extension for a given asset
379
+ #
380
+ # @example
381
+ #
382
+ # asset_normalize_extension(:images, "/foo/bar/baz.png") => "/foo/bar/baz.png"
383
+ # asset_normalize_extension(:js, "/foo/bar/baz") => "/foo/bar/baz.js"
384
+ #
385
+ def asset_normalize_extension(kind, source)
386
+ ignore_extension = !APPEND_ASSET_EXTENSIONS.include?(kind.to_s)
387
+ source << ".#{kind}" unless ignore_extension or source =~ /\.#{kind}/
388
+ source
389
+ end
390
+
399
391
  ##
400
392
  # Parses link_to options for given correct conditions
401
393
  #
@@ -0,0 +1,171 @@
1
+ module Padrino
2
+ module Helpers
3
+ class Breadcrumb
4
+
5
+ attr_accessor :home
6
+ attr_accessor :items
7
+
8
+ DEFAULT_URL = "/"
9
+ DEFAULT_CAPTION ="Home Page"
10
+
11
+ ##
12
+ # initialize breadcrumbs with default value
13
+ #
14
+ # @example
15
+ # before do
16
+ # @breadcrumbs = breadcrumbs.new
17
+ # end
18
+ #
19
+ # @api public
20
+ def initialize
21
+ self.home = { :url => DEFAULT_URL, :caption => DEFAULT_CAPTION, :name => :home }
22
+ reset
23
+ end
24
+
25
+ ##
26
+ # Set the custom home (Parent) link
27
+ #
28
+ # @param [String] url
29
+ # The url href
30
+ #
31
+ # @param [String] caption
32
+ # The text caption.
33
+ #
34
+ # @example
35
+ # breadcrumbs.set_home "/HomeFoo", "Foo Home"
36
+ #
37
+ #
38
+ # @api public
39
+ def set_home(url, caption)
40
+ self.home = { :url => url, :caption => caption.to_s.humanize, :name => :home }
41
+ reset
42
+ end
43
+
44
+ ##
45
+ # Reset breadcrumbs to default or personal home
46
+ #
47
+ # @example
48
+ # breadcrumbs.reset
49
+ #
50
+ # @api public
51
+ def reset
52
+ self.items=[]
53
+ self.items << home
54
+ end
55
+
56
+ ##
57
+ # Reset breadcrumbs to default home
58
+ #
59
+ # @example
60
+ # breadcrumbs.reset!
61
+ #
62
+ # @api public
63
+ def reset!
64
+ self.home = { :url => DEFAULT_URL, :caption => DEFAULT_CAPTION, :name => :home }
65
+ reset
66
+ end
67
+
68
+ ##
69
+ # Add a new breadcrumbs
70
+ #
71
+ # @param [String] name
72
+ # The name of resource
73
+ # @param [Symbol] name
74
+ # The name of resource
75
+ #
76
+ # @param [String] url
77
+ # The url href.
78
+ #
79
+ # @param [String] caption
80
+ # The text caption
81
+ #
82
+ # @example
83
+ # breadcrumbs.add "foo", "/foo", "Foo Link"
84
+ # breadcrumbs.add :foo, "/foo", "Foo Link"
85
+ #
86
+ # @api public
87
+ def add(name, url, caption)
88
+ items << { :name => name, :url => url.to_s, :caption => caption.to_s.humanize }
89
+ end
90
+
91
+ alias :<< :add
92
+
93
+ ##
94
+ # Remove a Breadcrumbs
95
+ #
96
+ # @param [String] name
97
+ # The name of resource to delete from breadcrumbs list
98
+ #
99
+ # @param [Symbol] name
100
+ # The name of resource to delete from breadcrumbs list
101
+ #
102
+ # @example
103
+ # breadcrumbs.del "foo"
104
+ # breadcrumbs.del :foo
105
+ #
106
+ # @api public
107
+ def del(name)
108
+ items.delete_if { |item| item[:name] == name.to_sym }
109
+ end
110
+
111
+ end # Breadcrumb
112
+
113
+
114
+ module Breadcrumbs
115
+
116
+ # Render breadcrumbs to view
117
+ #
118
+ # @param [Breadcrumbs] breadcrumbs
119
+ # The breadcrumbs to render into view
120
+ #
121
+ # @param [Boolean] bootstrap
122
+ # If true, render separation (usefull with Twitter Bootstrap)
123
+ #
124
+ # @param [String] active
125
+ # Css class style set to active breadcrumb
126
+ #
127
+ # @return [String] Unordered list with breadcrumbs
128
+ #
129
+ # @example
130
+ # = breadcrumbs @breacrumbs
131
+ # # Generates:
132
+ # # <ul>
133
+ # # <li><a herf="/foo" >Foo Link</a></li>
134
+ # # <li class="active" ><a herf="/bar">Bar Link</a></li>
135
+ # # </ul>
136
+ #
137
+ #
138
+ # @api public
139
+ def breadcrumbs(breadcrumbs, bootstrap=false, active="active")
140
+ content=""
141
+ breadcrumbs.items[0..-2].each do |item|
142
+ content << render_item(item, bootstrap)
143
+ end
144
+ last = link_to(breadcrumbs.items.last[:caption], breadcrumbs.items.last[:url])
145
+ content << safe_content_tag(:li, last, :class => active)
146
+ safe_content_tag(:ul, content, :class => "breadcrumb" )
147
+ end
148
+
149
+ private
150
+ ##
151
+ # Private method to return list item
152
+ #
153
+ # @param [Hash] item
154
+ # The breadcrumb item
155
+ #
156
+ # @param [Boolean] bootstrap
157
+ # If true, render separation (usefull with Twitter Bootstrap)
158
+ #
159
+ # @return [String] List item with breacrumb
160
+ #
161
+ # @api public
162
+ def render_item(item, bootstrap)
163
+ content = ""
164
+ content << link_to(item[:caption], item[:url])
165
+ content << safe_content_tag(:span, "/", :class => "divider") if bootstrap
166
+ safe_content_tag(:li, content )
167
+ end
168
+
169
+ end # Breadcrumb
170
+ end # Helpers
171
+ end # Padrino
@@ -95,12 +95,38 @@ module Padrino
95
95
  @template.select_tag field_name(field), options
96
96
  end
97
97
 
98
+ # f.check_box_group :color, :options => ['red', 'green', 'blue'], :selected => ['red', 'blue']
99
+ # f.check_box_group :color, :collection => @colors, :fields => [:name, :id]
100
+ def check_box_group(field, options={})
101
+ selected_values = Array(options[:selected] || field_value(field))
102
+ if options[:collection]
103
+ fields = options[:fields] || [:name, :id]
104
+ # don't use map!, it will break some orms
105
+ selected_values = selected_values.map{ |v| (v.respond_to?(fields[0]) ? v.send(fields[1]) : v).to_s }
106
+ end
107
+ labeled_group( field, options ) do |variant|
108
+ @template.check_box_tag( field_name(field)+'[]', :value => variant[1], :id => variant[2], :checked => selected_values.include?(variant[1]) )
109
+ end
110
+ end
111
+
112
+ # f.radio_button_group :color, :options => ['red', 'green']
113
+ # f.radio_button_group :color, :collection => @colors, :fields => [:name, :id], :selected => @colors.first
114
+ def radio_button_group(field, options={})
115
+ fields = options[:fields] || [:name, :id]
116
+ selected_value = options[:selected] || field_value(field)
117
+ selected_value = selected_value.send(fields[1]) if selected_value.respond_to?(fields[0])
118
+ labeled_group( field, options ) do |variant|
119
+ @template.radio_button_tag( field_name(field), :value => variant[1], :id => variant[2], :checked => variant[1] == selected_value.to_s )
120
+ end
121
+ end
122
+
98
123
  # f.check_box :remember_me, :value => 'true', :uncheck_value => '0'
99
124
  def check_box(field, options={})
125
+ html = ActiveSupport::SafeBuffer.new
100
126
  unchecked_value = options.delete(:uncheck_value) || '0'
101
127
  options.reverse_merge!(:id => field_id(field), :value => '1')
102
128
  options.reverse_merge!(:checked => true) if values_matches_field?(field, options[:value])
103
- html = @template.hidden_field_tag(options[:name] || field_name(field), :value => unchecked_value, :id => nil)
129
+ html << @template.hidden_field_tag(options[:name] || field_name(field), :value => unchecked_value, :id => nil)
104
130
  html << @template.check_box_tag(field_name(field), options)
105
131
  end
106
132
 
@@ -140,7 +166,11 @@ module Padrino
140
166
  result = nested_objects.each_with_index.map do |child_instance, index|
141
167
  nested_options[:index] = include_index ? index : nil
142
168
  @template.fields_for(child_instance, { :nested => nested_options }, &block)
143
- end.join("\n")
169
+ end.join("\n").html_safe
170
+ end
171
+
172
+ def csrf_token_field
173
+ @template.csrf_token_field
144
174
  end
145
175
 
146
176
  protected
@@ -171,17 +201,8 @@ module Padrino
171
201
  # field_name(:number) => "user[telephone_attributes][number]"
172
202
  # field_name(:street) => "user[addresses_attributes][0][street]"
173
203
  def field_name(field=nil)
174
- result = []
175
- if root_form?
176
- result << object_model_name
177
- elsif nested_form?
178
- parent_form = @options[:nested][:parent]
179
- attributes_name = "#{@options[:nested][:association]}_attributes"
180
- nested_index = @options[:nested][:index]
181
- fragment = [parent_form.field_name, "[#{attributes_name}", "]"]
182
- fragment.insert(2, "][#{nested_index}") if nested_index
183
- result << fragment
184
- end
204
+ result = field_result
205
+ result << field_name_fragment if nested_form?
185
206
  result << "[#{field}]" unless field.blank?
186
207
  result.flatten.join
187
208
  end
@@ -192,17 +213,8 @@ module Padrino
192
213
  # field_name(:number) => "user_telephone_attributes_number"
193
214
  # field_name(:street) => "user_addresses_attributes_0_street"
194
215
  def field_id(field=nil, value=nil)
195
- result = []
196
- if root_form?
197
- result << object_model_name
198
- elsif nested_form?
199
- parent_form = @options[:nested][:parent]
200
- attributes_name = "#{@options[:nested][:association]}_attributes"
201
- nested_index = @options[:nested][:index]
202
- fragment = [parent_form.field_id, "_#{attributes_name}"]
203
- fragment.push("_#{nested_index}") if nested_index
204
- result << fragment
205
- end
216
+ result = field_result
217
+ result << field_id_fragment if nested_form?
206
218
  result << "_#{field}" unless field.blank?
207
219
  result << "_#{value}" unless value.blank?
208
220
  result.flatten.join
@@ -245,6 +257,52 @@ module Padrino
245
257
  def root_form?
246
258
  !nested_form?
247
259
  end
260
+
261
+ # Builds a group of labels for radios or checkboxes
262
+ def labeled_group(field, options={})
263
+ options.reverse_merge!(:id => field_id(field), :selected => field_value(field))
264
+ options.merge!(:class => field_error(field, options))
265
+ variants = case
266
+ when options[:options]
267
+ options[:options].map{ |caption, value| [caption.to_s, (value||caption).to_s] }
268
+ when options[:collection]
269
+ fields = options[:fields] || [:name, :id]
270
+ options[:collection].map{ |variant| [variant.send(fields.first).to_s, variant.send(fields.last).to_s] }
271
+ else
272
+ []
273
+ end
274
+ variants.inject('') do |html, variant|
275
+ variant[2] = "#{field_id(field)}_#{variant[1]}"
276
+ html << @template.label_tag("#{field_name(field)}[]", :for => variant[2], :caption => "#{yield(variant)} #{variant[0]}")
277
+ end
278
+ end
279
+
280
+ private
281
+ def field_result
282
+ result = []
283
+ result << object_model_name if root_form?
284
+ result
285
+ end
286
+
287
+ def field_name_fragment
288
+ fragment = [result_options[:parent_form].field_name, "[#{result_options[:attributes_name]}", "]"]
289
+ fragment.insert(2, "][#{result_options[:nested_index]}") if result_options[:nested_index]
290
+ fragment
291
+ end
292
+
293
+ def field_id_fragment
294
+ fragment = [result_options[:parent_form].field_id, "_#{result_options[:attributes_name]}"]
295
+ fragment.push("_#{result_options[:nested_index]}") if result_options[:nested_index]
296
+ fragment
297
+ end
298
+
299
+ def result_options
300
+ {
301
+ :parent_form => @options[:nested][:parent],
302
+ :nested_index => @options[:nested][:index],
303
+ :attributes_name => "#{@options[:nested][:association]}_attributes"
304
+ }
305
+ end
248
306
  end # AbstractFormBuilder
249
307
  end # FormBuilder
250
308
  end # Helpers