From faba406fa15251cdc9588364d23c687a14ed6885 Mon Sep 17 00:00:00 2001 From: Bogdan Gusiev Date: Wed, 8 Jun 2011 12:59:54 +0300 Subject: Fixing select[multiple] html specification problem. Generating hidden input with same name before each multiple select --- .../lib/action_view/helpers/form_options_helper.rb | 43 +++++++++++++++++++--- .../test/template/form_options_helper_test.rb | 20 +++++++++- 2 files changed, 55 insertions(+), 8 deletions(-) (limited to 'actionpack') diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 6513edcf6e..97b9d0a578 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -128,6 +128,28 @@ module ActionView # By default, post.person_id is the selected option. Specify :selected => value to use a different selection # or :selected => nil to leave all options unselected. Similarly, you can specify values to be disabled in the option # tags by specifying the :disabled option. This can either be a single value or an array of values to be disabled. + # + # ==== Gotcha + # + # The HTML specification says when +multiple+ parameter passed to select and all options got deselected + # web browsers do not send any value to server. Unfortunately this introduces a gotcha: + # if an +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user + # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So, + # any mass-assignment idiom like + # + # @user.update_attributes(params[:user]) + # + # wouldn't update roles. + # + # To prevent this the helper generates an auxiliary hidden field before + # every multiple select. The hidden field has the same name as multiple select and blank value. + # + # This way, the client either sends only the hidden field (representing + # the deselected multiple select box), or both fields. Since the HTML specification + # says key/value pairs have to be sent in the same order they appear in the + # form, and parameters extraction gets the last occurrence of any repeated + # key in the query string, that works for ordinary forms. + # def select(object, method, choices, options = {}, html_options = {}) InstanceTag.new(object, method, self, options.delete(:object)).to_select_tag(choices, options, html_options) end @@ -557,7 +579,7 @@ module ActionView value = value(object) selected_value = options.has_key?(:selected) ? options[:selected] : value disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil - content_tag("select", add_options(options_for_select(choices, :selected => selected_value, :disabled => disabled_value), options, selected_value), html_options) + select_content_tag(add_options(options_for_select(choices, :selected => selected_value, :disabled => disabled_value), options, selected_value), html_options) end def to_collection_select_tag(collection, value_method, text_method, options, html_options) @@ -566,8 +588,8 @@ module ActionView value = value(object) disabled_value = options.has_key?(:disabled) ? options[:disabled] : nil selected_value = options.has_key?(:selected) ? options[:selected] : value - content_tag( - "select", add_options(options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => disabled_value), options, value), html_options + select_content_tag( + add_options(options_from_collection_for_select(collection, value_method, text_method, :selected => selected_value, :disabled => disabled_value), options, value), html_options ) end @@ -575,8 +597,8 @@ module ActionView html_options = html_options.stringify_keys add_default_name_and_id(html_options) value = value(object) - content_tag( - "select", add_options(option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, value), options, value), html_options + select_content_tag( + add_options(option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, value), options, value), html_options ) end @@ -584,7 +606,7 @@ module ActionView html_options = html_options.stringify_keys add_default_name_and_id(html_options) value = value(object) - content_tag("select", + select_content_tag( add_options( time_zone_options_for_select(value || options[:default], priority_zones, options[:model] || ActiveSupport::TimeZone), options, value @@ -603,6 +625,15 @@ module ActionView end option_tags.html_safe end + + def select_content_tag(content, html_options) + select = content_tag("select", content, html_options) + if html_options["multiple"] + tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select + else + select + end + end end class FormBuilder diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index b92e1d9890..f3969895ae 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -457,6 +457,22 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_multiple_to_add_hidden_input + output_buffer = select(:post, :category, "", {}, :multiple => true) + assert_dom_equal( + "", + output_buffer + ) + end + + def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input + output_buffer = select(:post, :category, "", {}, :multiple => true, :disabled => true) + assert_dom_equal( + "", + output_buffer + ) + end + def test_select_with_blank @post = Post.new @post.category = "" @@ -649,11 +665,11 @@ class FormOptionsHelperTest < ActionView::TestCase ) end - def test_collection_select_with_multiple_option_appends_array_brackets + def test_collection_select_with_multiple_option_appends_array_brackets_and_hidden_input @post = Post.new @post.author_name = "Babe" - expected = "" + expected = "" # Should suffix default name with []. assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, :multiple => true) -- cgit v1.2.3