Permalink
Browse files

Merge pull request #4851 from carlosantoniodasilva/collection-helpers

Collection radio buttons / check boxes helpers
  • Loading branch information...
2 parents 215a390 + 5d8191a commit eb18260565bf3b271f2150b02dfadb4071a4f551 @josevalim josevalim committed Feb 2, 2012
View
@@ -1,13 +1,49 @@
## Rails 4.0.0 (unreleased) ##
+* Allow `value_method` and `text_method` arguments from `collection_select` and
+ `options_from_collection_for_select` to receive an object that responds to `:call`,
+ such as a `proc`, to evaluate the option in the current element context. This works
+ the same way with `collection_radio_buttons` and `collection_check_boxes`.
+
+ *Carlos Antonio da Silva + Rafael Mendonça França*
+
+* Add `collection_check_boxes` form helper, similar to `collection_select`:
+ Example:
+
+ collection_check_boxes :post, :author_ids, Author.all, :id, :name
+ # Outputs something like:
+ <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" />
+ <label for="post_author_ids_1">D. Heinemeier Hansson</label>
+ <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
+ <label for="post_author_ids_2">D. Thomas</label>
+ <input name="post[author_ids][]" type="hidden" value="" />
+
+ The label/check_box pairs can be customized with a block.
+
+ *Carlos Antonio da Silva + Rafael Mendonça França*
+
+* Add `collection_radio_buttons` form helper, similar to `collection_select`:
+ Example:
+
+ collection_radio_buttons :post, :author_id, Author.all, :id, :name
+ # Outputs something like:
+ <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" />
+ <label for="post_author_id_1">D. Heinemeier Hansson</label>
+ <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
+ <label for="post_author_id_2">D. Thomas</label>
+
+ The label/radio_button pairs can be customized with a block.
+
+ *Carlos Antonio da Silva + Rafael Mendonça França*
+
* check_box with `:form` html5 attribute will now replicate the `:form`
attribute to the hidden field as well. *Carlos Antonio da Silva*
* `label` form helper accepts :for => nil to not generate the attribute. *Carlos Antonio da Silva*
* Add `:format` option to number_to_percentage *Rodrigo Flores*
-* Add `config.action_view.logger` to configure logger for ActionView. *Rafael França*
+* Add `config.action_view.logger` to configure logger for ActionView. *Rafael Mendonça França*
* Deprecated ActionController::Integration in favour of ActionDispatch::Integration
@@ -164,7 +164,9 @@ def select(object, method, choices, options = {}, html_options = {})
#
# The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
# of +collection+. The return values are used as the +value+ attribute and contents of each
- # <tt><option></tt> tag, respectively.
+ # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such
+ # as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the value/text.
#
# Example object structure for use with this method:
# class Post < ActiveRecord::Base
@@ -360,12 +362,13 @@ def options_for_select(container, selected = nil)
# should produce the desired results.
def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
options = collection.map do |element|
- [element.send(text_method), element.send(value_method)]
+ [value_for_collection(element, text_method), value_for_collection(element, value_method)]
end
selected, disabled = extract_selected_and_disabled(selected)
- select_deselect = {}
- select_deselect[:selected] = extract_values_from_collection(collection, value_method, selected)
- select_deselect[:disabled] = extract_values_from_collection(collection, value_method, disabled)
+ select_deselect = {
+ :selected => extract_values_from_collection(collection, value_method, selected),
+ :disabled => extract_values_from_collection(collection, value_method, disabled)
+ }
options_for_select(options, select_deselect)
end
@@ -418,10 +421,10 @@ def options_from_collection_for_select(collection, value_method, text_method, se
# wrap the output in an appropriate <tt><select></tt> tag.
def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
collection.map do |group|
- group_label_string = group.send(group_label_method)
- "<optgroup label=\"#{ERB::Util.html_escape(group_label_string)}\">" +
- options_from_collection_for_select(group.send(group_method), option_key_method, option_value_method, selected_key) +
- '</optgroup>'
+ option_tags = options_from_collection_for_select(
+ group.send(group_method), option_key_method, option_value_method, selected_key)
+
+ content_tag(:optgroup, option_tags, :label => group.send(group_label_method))
end.join.html_safe
end
@@ -519,14 +522,138 @@ def time_zone_options_for_select(selected = nil, priority_zones = nil, model = :
zone_options.html_safe
end
+ # Returns radio button tags for the collection of existing return values
+ # of +method+ for +object+'s class. The value returned from calling
+ # +method+ on the instance +object+ will be selected. If calling +method+
+ # returns +nil+, no selection is made.
+ #
+ # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
+ # methods to be called on each member of +collection+. The return values
+ # are used as the +value+ attribute and contents of each radio button tag,
+ # respectively. They can also be any object that responds to +call+, such
+ # as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the value/text.
+ #
+ # Example object structure for use with this method:
+ # class Post < ActiveRecord::Base
+ # belongs_to :author
+ # end
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # def name_with_initial
+ # "#{first_name.first}. #{last_name}"
+ # end
+ # end
+ #
+ # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
+ #
+ # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
+ # <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" />
+ # <label for="post_author_id_1">D. Heinemeier Hansson</label>
+ # <input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
+ # <label for="post_author_id_2">D. Thomas</label>
+ # <input id="post_author_id_3" name="post[author_id]" type="radio" value="3" />
+ # <label for="post_author_id_3">M. Clark</label>
+ #
+ # It is also possible to customize the way the elements will be shown by
+ # giving a block to the method:
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
+ # b.label { b.radio_button }
+ # end
+ #
+ # The argument passed to the block is a special kind of builder for this
+ # collection, which has the ability to generate the label and radio button
+ # for the current item in the collection, with proper text and value.
+ # Using it, you can change the label and radio button display order or
+ # even use the label as wrapper, as in the example above.
+ #
+ # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept
+ # extra html options:
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
+ # b.label(:class => "radio_button") { b.radio_button(:class => "radio_button") }
+ # end
+ #
+ # There are also two special methods available: <tt>text</tt> and
+ # <tt>value</tt>, which are the current text and value methods for the
+ # item being rendered, respectively. You can use them like this:
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
+ # b.label(:"data-value" => b.value) { b.radio_button + b.text }
+ # end
+ def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
+ Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
+ end
+
+ # Returns check box tags for the collection of existing return values of
+ # +method+ for +object+'s class. The value returned from calling +method+
+ # on the instance +object+ will be selected. If calling +method+ returns
+ # +nil+, no selection is made.
+ #
+ # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
+ # methods to be called on each member of +collection+. The return values
+ # are used as the +value+ attribute and contents of each check box tag,
+ # respectively. They can also be any object that responds to +call+, such
+ # as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the value/text.
+ #
+ # Example object structure for use with this method:
+ # class Post < ActiveRecord::Base
+ # has_and_belongs_to_many :author
+ # end
+ # class Author < ActiveRecord::Base
+ # has_and_belongs_to_many :posts
+ # def name_with_initial
+ # "#{first_name.first}. #{last_name}"
+ # end
+ # end
+ #
+ # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
+ #
+ # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return:
+ # <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" />
+ # <label for="post_author_ids_1">D. Heinemeier Hansson</label>
+ # <input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
+ # <label for="post_author_ids_2">D. Thomas</label>
+ # <input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" />
+ # <label for="post_author_ids_3">M. Clark</label>
+ # <input name="post[author_ids][]" type="hidden" value="" />
+ #
+ # It is also possible to customize the way the elements will be shown by
+ # giving a block to the method:
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
+ # b.label { b.check_box }
+ # end
+ #
+ # The argument passed to the block is a special kind of builder for this
+ # collection, which has the ability to generate the label and check box
+ # for the current item in the collection, with proper text and value.
+ # Using it, you can change the label and check box display order or even
+ # use the label as wrapper, as in the example above.
+ #
+ # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept
+ # extra html options:
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
+ # b.label(:class => "check_box") { b.check_box(:class => "check_box") }
+ # end
+ #
+ # There are also two special methods available: <tt>text</tt> and
+ # <tt>value</tt>, which are the current text and value methods for the
+ # item being rendered, respectively. You can use them like this:
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
+ # b.label(:"data-value" => b.value) { b.check_box + b.text }
+ # end
+ def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
+ Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
+ end
+
private
def option_html_attributes(element)
return "" unless Array === element
- html_attributes = []
- element.select { |e| Hash === e }.reduce({}, :merge).each do |k, v|
- html_attributes << " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\""
- end
- html_attributes.join
+
+ element.select { |e| Hash === e }.reduce({}, :merge).map do |k, v|
+ " #{k}=\"#{ERB::Util.html_escape(v.to_s)}\""
+ end.join
end
def option_text_and_value(option)
@@ -552,12 +679,12 @@ def option_value_selected?(value, selected)
def extract_selected_and_disabled(selected)
if selected.is_a?(Proc)
- [ selected, nil ]
+ [selected, nil]
else
selected = Array.wrap(selected)
options = selected.extract_options!.symbolize_keys
- selected_items = options.include?(:selected) ? options[:selected] : selected
- [ selected_items, options[:disabled] ]
+ selected_items = options.fetch(:selected, selected)
+ [selected_items, options[:disabled]]
end
end
@@ -570,6 +697,10 @@ def extract_values_from_collection(collection, value_method, selected)
selected
end
end
+
+ def value_for_collection(item, value)
+ value.respond_to?(:call) ? value.call(item) : item.send(value)
+ end
end
class FormBuilder
@@ -588,6 +719,14 @@ def grouped_collection_select(method, collection, group_method, group_label_meth
def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
@template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
end
+
+ def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {})
+ @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
+ end
+
+ def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {})
+ @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
+ end
end
end
end
@@ -4,27 +4,29 @@ module Tags #:nodoc:
extend ActiveSupport::Autoload
autoload :Base
+ autoload :CheckBox
+ autoload :CollectionCheckBoxes
+ autoload :CollectionRadioButtons
+ autoload :CollectionSelect
+ autoload :DateSelect
+ autoload :DatetimeSelect
+ autoload :EmailField
+ autoload :FileField
+ autoload :GroupedCollectionSelect
+ autoload :HiddenField
autoload :Label
- autoload :TextField
+ autoload :NumberField
autoload :PasswordField
- autoload :HiddenField
- autoload :FileField
+ autoload :RadioButton
+ autoload :RangeField
autoload :SearchField
+ autoload :Select
autoload :TelField
- autoload :UrlField
- autoload :EmailField
- autoload :NumberField
- autoload :RangeField
autoload :TextArea
- autoload :CheckBox
- autoload :RadioButton
- autoload :Select
- autoload :CollectionSelect
- autoload :GroupedCollectionSelect
- autoload :TimeZoneSelect
- autoload :DateSelect
+ autoload :TextField
autoload :TimeSelect
- autoload :DatetimeSelect
+ autoload :TimeZoneSelect
+ autoload :UrlField
end
end
end
@@ -32,9 +32,11 @@ def value(object)
def value_before_type_cast(object)
unless object.nil?
- object.respond_to?(@method_name + "_before_type_cast") ?
- object.send(@method_name + "_before_type_cast") :
- object.send(@method_name)
+ method_before_type_cast = @method_name + "_before_type_cast"
+
+ object.respond_to?(method_before_type_cast) ?
+ object.send(method_before_type_cast) :
+ object.send(@method_name)
end
end
@@ -59,13 +61,15 @@ def retrieve_autoindex(pre_match)
end
def add_default_name_and_id_for_value(tag_value, options)
- unless tag_value.nil?
- pretty_tag_value = tag_value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
- specified_id = options["id"]
+ if tag_value.nil?
add_default_name_and_id(options)
- options["id"] += "_#{pretty_tag_value}" if specified_id.blank? && options["id"].present?
else
+ specified_id = options["id"]
add_default_name_and_id(options)
+
+ if specified_id.blank? && options["id"].present?
+ options["id"] += "_#{sanitized_value(tag_value)}"
+ end
end
end
@@ -78,7 +82,7 @@ def add_default_name_and_id(options)
options["name"] ||= tag_name_with_index(@auto_index)
options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
else
- options["name"] ||= tag_name + (options['multiple'] ? '[]' : '')
+ options["name"] ||= options['multiple'] ? tag_name_multiple : tag_name
options["id"] = options.fetch("id"){ tag_id }
end
options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
@@ -88,6 +92,10 @@ def tag_name
"#{@object_name}[#{sanitized_method_name}]"
end
+ def tag_name_multiple
+ "#{tag_name}[]"
+ end
+
def tag_name_with_index(index)
"#{@object_name}[#{index}][#{sanitized_method_name}]"
end
@@ -108,6 +116,10 @@ def sanitized_method_name
@sanitized_method_name ||= @method_name.sub(/\?$/,"")
end
+ def sanitized_value(value)
+ value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
+ end
+
def select_content_tag(option_tags, options, html_options)
html_options = html_options.stringify_keys
add_default_name_and_id(html_options)
Oops, something went wrong.

0 comments on commit eb18260

Please sign in to comment.