89

I'm using Rails 3.2.0.rc2. I've got a Model, in which I have a static Array which I'm offering up through a form such that users may select a subset of Array and save their selection to the database, stored in a single column in Model. I've used serialize on the database column which stores the Array and Rails is correctly converting the users' selections into Yaml (and back to an array when reading that column). I'm using a multi-select form input to make selections.

My problem is that, the way I currently have it, everything works as I would expect except that the user's subset array always has a blank first element when it's sent to the server.

This isn't a big deal, and I could write code to cut that out after the fact, but I feel like I'm just making some kind of syntactical error as it doesn't seem to me that the default Rails behaviour would intentionally add this blank element without some reason. I must have missed something or forgot to disable some kind of setting. Please help me understand what I'm missing (or point me in to some good documentation that describes this with more depth than what I've been able to find on the intertubes).

MySQL Database Table 'models':

  • includes a column named subset_array which is a TEXT field

Class Model includes the following settings:

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

Form for editing Models includes the following input option:

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

PUT to server from client looks something like this:

  • assuming only value1 and value3 are selected
  • "model" => { "subset_array" => ["", value1, value3] }

Database update looks like this:

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

As you can see, there's this extra, blank, element in the array being sent and set in the database. How do I get rid of that? Is there a parameter I'm missing from my f.select call?

Much thanks appreciated :)

EDIT: This is the generated HTML code from the f.select statement. It looks as though there is a hidden input being generated which may be the cause of my issue? Why is that there?

<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>
robmclarty
  • 2,215
  • 2
  • 20
  • 21
  • Could you post the HTML snippet that `f.select` is generating? Also, does this behaviour happen even on create, or is it just on update? – Mike A. Jan 19 '12 at 17:22
  • Added EDIT of output HTML markup generated from `f.select` – robmclarty Jan 19 '12 at 18:52
  • @mike-a Confirmed same behaviour for both create and update – robmclarty Jan 19 '12 at 19:01
  • I wondered if the browser I was using might be part of the problem: how it interprets and expresses the meaning of the hidden input tag with the same name as the select tag. So I tried my app in Chrome, Safari, Firefox, and Opera, and each produced the same results. – robmclarty Jan 20 '12 at 04:52
  • 3
    Do note, all the solutions that use `include_hidden: false` comes with a gotcha. When you remove all values from the select box, the idiomatic `model.update(something_params)` will not include that field. TL;DR you won't be able to make the field empty. – Damon Aw May 06 '14 at 15:56

9 Answers9

69

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.

Bogdan Gusiev
  • 8,027
  • 16
  • 61
  • 81
  • Thanks Bogdan. I think this is the kind of thing I will be implementing in my app to work around the issue. This is a lot easier to do than trying to work around issues with user-agents' HTML spec implementations or something. – robmclarty Jan 20 '12 at 15:33
  • Bring your concerns to rails core team members. They are authorized to review and accept patches and take responsibilities for consequent issues. – Bogdan Gusiev Jan 22 '13 at 13:20
  • Bogdan, is there anyway to turn off the hidden field, if multiple is true? This breaks quite a bit of stuff in my app when upgrading to 3.2. I really don't like the fact that I have to clean stuff up in the controller because of Rails magic adding extra empty values. – taelor Mar 05 '13 at 19:51
  • Update my answer with upcomming info from Rails 4 – Bogdan Gusiev Mar 06 '13 at 13:42
  • @donato you should submit an issue to rails than, because this feature is documented. – Bogdan Gusiev Feb 12 '15 at 10:18
  • 2
    @Donato you must set include_hidden to false (include_hidden: false) – Florian Widtmann Feb 17 '15 at 10:13
  • if using formtastic - do not put it in the input_html parameter e.g. ` = f.input :genders, as: :select, collection: gender_options, input_html: {multiple: true, include_blank: false}, include_hidden: false` – ryan2johnson9 May 19 '16 at 06:22
  • I think this functionality should be in the view or the controller, not in the model – sekmo Jan 30 '19 at 08:53
57

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.

Mike A.
  • 3,189
  • 22
  • 20
  • 1
    I've created [an example app](https://github.com/robmclarty/Select-Multiple-Empty-Element) to demonstrate the issue while I try to figure out how to handle it properly :P – robmclarty Jan 20 '12 at 01:27
  • 1
    I feel like the bug is not necessarily in Rails, but in an ambiguous specification and implementation for this particular element functionality. How should a user-agent express a change of state of *newly empty* to the form processor if empty (or non-selected) form elements are considered *not* to be [successful controls](http://www.w3.org/TR/html4/interact) and thus *not* submitted with the form contents? – robmclarty Jan 20 '12 at 15:39
  • So, if this is a bug, is it documented in Rails issue tracker? – bmihelac Apr 15 '12 at 17:13
  • Somebody else answered this below; in Rails 4+ you need to use `include_hidden: false` – Allison Jun 28 '21 at 23:37
24

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 } %>
Martin
  • 351
  • 3
  • 8
10

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.

Max
  • 109
  • 2
5

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
devishot
  • 389
  • 5
  • 5
2

In the controller:

arr = arr.delete_if { |x| x.empty? }
Arslan Ali
  • 17,418
  • 8
  • 58
  • 76
Mauro
  • 1,225
  • 2
  • 8
  • 9
0

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
Bruno
  • 6,211
  • 16
  • 69
  • 104
-1

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.

Arslan Ali
  • 17,418
  • 8
  • 58
  • 76
imaginabit
  • 409
  • 4
  • 9
-3

Use jQuery:

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

Option to remove blank options from drop down.

Arslan Ali
  • 17,418
  • 8
  • 58
  • 76
user1875926
  • 121
  • 11