为什么我的 Rails 中的第一个元素总是使用嵌入式数组进行多重选择?

我用的是 Rails3.2.0. rc2。我有一个 Model,其中我有一个静态的 Array,我提供了一个形式,使用户可以选择一个子集的 Array和保存他们的选择到数据库中,存储在一个单一的列在 Model。我已经使用序列化的数据库列存储的 Array和 Rails 是正确的转换用户的选择到 Yaml (并返回到一个数组读取该列)。我使用多选表单输入来进行选择。

我的问题是,按照我目前的方式,除了用户的子集数组在发送到服务器时总是有一个空白的第一个元素之外,所有东西都按照我预期的方式工作。

这没什么大不了的,我可以在事后编写代码来删除它,但我觉得我只是犯了某种语法错误,因为在我看来,默认的 Rails 行为不会无缘无故地故意添加这个空白元素。我一定是遗漏了什么或者忘了关闭某种设置。请帮助我理解我错过了什么(或者给我指出一些好的文档,这些文档比我在互联网上找到的更深入地描述了这一点)。

MySQL 数据库表“模型”:

  • 包含一个名为 subset_array的列,它是一个 TEXT 字段

类模型包括以下设置:

  • serialize :subset_array
  • ALL_POSSIBLE_VALUES = [value1, value2, value3, ...]

编辑表格模型包括以下输入选项:

  • f.select :subset_array, Model::ALL_POSSIBLE_VALUES, {}, :multiple => true, :selected => @model.subset_array

从客户端到服务器的 PUT 如下所示:

  • 假设只选择 value1和 value3
  • "model" => { "subset_array" => ["", value1, value3] }

数据库更新如下:

  • UPDATE 'models' SET 'subset_array' = '--- \n- \"\"\n- value1\n- value3\n'

如您所见,在数据库中发送和设置的数组中有这个额外的空白元素。我怎么才能摆脱它?我的 f.select调用中是否缺少一个参数?

非常感谢:)

EDIT : 这是从 f.select语句生成的 HTML 代码。看起来好像有一个隐藏的输入正在生成,这可能是我的问题的原因?为什么在那里?

<input name="model[subset_array][]" type="hidden" value>
<select id="model_subset_array" multiple="multiple" name="model[subset_array][]" selected="selected">
<option value="value1" selected="selected">Value1</option>
<option value="value2">Value2</option>
<option value="value3" selected="selected">Value3</option>
<option...>...</option>
</select>
30629 次浏览

The hidden field is what is causing the issue. But it is there for a good reason: when all values are deselected, you still receive a subset_array parameter. From the Rails docs (you may have to scroll to the right to see all of this):

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

EDIT: The last paragraph suggests that you shouldn't be seeing the empty one in the case when something is selected, but I think it is wrong. The person who made this commit to Rails (see https://github.com/rails/rails/commit/faba406fa15251cdc9588364d23c687a14ed6885) is trying to do the same trick that Rails uses for checkboxes (as mentioned here: https://github.com/rails/rails/pull/1552), but I don't think it can work for a multiple select box because the parameters sent over form an array in this case and so no value is ignored.

So my feeling is that this is a bug.

In Rails 4:

You will be able to pass :include_hidden option. https://github.com/rails/rails/pull/5414/files

As a quick fix for now: you can use right now in your model:

before_validation do |model|
model.subset_array.reject!(&:blank?) if model.subset_array
end

This will just delete all blank values at model level.

Another quick fix is to use this controller filter:

def clean_select_multiple_params hash = params
hash.each do |k, v|
case v
when Array then v.reject!(&:blank?)
when Hash then clean_select_multiple_params(v)
end
end
end

This way can be reused across controllers without touching the model layer.

In the controller:

arr = arr.delete_if { |x| x.empty? }

I make it work by writing this in the Javascript part of the page:

$("#model_subset_array").val( <%= @model.subset_array %> );

Mine looks more like following:

$("#modela_modelb_ids").val( <%= @modela.modelb_ids %> );

Not sure if this is going to get me headache in the future but now it works fine.

Use jQuery:

$('select option:empty').remove();

Option to remove blank options from drop down.

http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-check_box

Gotcha

The HTML specification says unchecked check boxes or selects are not successful, and thus web browsers do not send them. Unfortunately this introduces a gotcha: if an Invoice model has a paid flag, and in the form that edits a paid invoice the user unchecks its check box, no paid parameter is sent. So, any mass-assignment idiom like

@invoice.update(params[:invoice]) wouldn't update the flag.

To prevent this the helper generates an auxiliary hidden field before the very check box. The hidden field has the same name and its attributes mimic an unchecked check box.

This way, the client either sends only the hidden field (representing the check box is unchecked), 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.

To remove blank values:

  def myfield=(value)
value.reject!(&:blank?)
write_attribute(:myfield, value)
end

I fixed it using the params[:review][:staff_ids].delete("") in the controller before the update.

In my view:

= form_for @review do |f|
= f.collection_select :staff_ids, @business.staff, :id, :full_name, {}, {multiple:true}
= f.submit 'Submit Review'

In my controller:

class ReviewsController < ApplicationController
def create
....
params[:review][:staff_ids].delete("")
@review.update_attribute(:staff_ids, params[:review][:staff_ids].join(","))
....
end
end

In Rails 4+ set :include_hidden on select_tag to false

<%= form.grouped_collection_select :employee_id, Company.all, :employees, :name, :id, :name, { include_hidden: false }, { size: 6, multiple: true } %>