loading...

The 'method' in collection_select

dianakw8591 profile image dianakw8591 ・4 min read

When using a form helper for choosing an option (or multiple options) from a selection, the method argument can be a big hang up. It has to be a method that can be called on the object, and is also a parameter passed into create back in the controller - very confusing! Below I’ll go through how the associations between your models and your selection options determine what method to use in your form helper and what will go in your allowed parameters.

Getting started:

Let’s work with a domain of pet owners, with two models: pets and owners. Here’s what we have so far:

Pet.create(species: "dog", name: "Pearl")
Pet.create(species: "cat", name: "Dasher")
Pet.create(species: "cat", name: "Yoda")

In this domain, an owner can have many pets and a pet can only belong to one owner.

class Owner < ApplicationRecord
    has_many :pets
end

class Pet < ApplicationRecord
    belongs_to :owner, optional: true
end

I wanted to create pets without assigning a foreign key immediately - that's what the optional: true is doing there.

The new.html.erb form

In the form for a new owner, I’ll use form_for and the collection_select form helper so that a new owner can choose from the current list of pets.

Remember the format for collection_select:

collection_select(object, method, collection, value_method, text_method, 
     options = {}, html_options = {})

In this case object will be the owner instance @owner, and I’ll write my form_for with a block that will create fields already connected to my object, like so:

<%= form_for @owner do |f| %>
    <%= f.label :name %>
    <%= f.text_field :name %><br>

    <%= f.label :pets %>
    <%= f.collection_select :pet_ids, Pet.all, :id, :name %><br>

    <%= f.submit %>
<% end %>

In this format, @owner is already connected to the FormBuilder object f so pet_ids (the method) is the first argument.

Finally, the html generated by collection_select looks like this:

<select name="owner[pet_ids]" id="owner_pet_ids">
   <option value="1">Pearl</option>
   <option value="2">Dasher</option>
   <option value="3">Yoda</option>
</select>

Digging into the Method

For starters, method has to be a method that can be called on my owner instance. We could use nil? (a method that can be called on any object) and my form would still render with the following html:

<select name="owner[nil]" id="owner_nil>

But that wouldn’t be very useful for us. We want a method that will create the appropriate associations after the selection: in this case, a pet should be associated with a new owner. Remember that defining the associations between models gives us some extra methods to use. For a has_many relationship, we’ve gained (among others) the following two methods:

pet_ids
pet_ids=(ids)

What about pet_id? If I try using this method, Rails yells at me with a NoMethodError and won’t even render my form.

This part is critical: Because an owner has_many pets, the method pet_ids must be plural! Doesn’t matter that we’re only able to select one pet from our drop-down menu. The method is pluralized because of our defined relationships. On the flip side, in my new pet form, the method I would use to assign an owner to a new pet would be owner_id. A pet belongs_to a single owner, and therefore the method call is also singular.

Ok, we’ve got a method. What happens next? Back in our controller, we’re going to pass in our allowed parameters to create a new owner instance. Here are the parameters I’m allowing:

def owner_params
    params.require(:owner).permit(:name, :pet_ids)
end

Notice that again, pet_ids is plural here, and also that is is not an array of pet_ids. Because collection_select is only allowing us to choose one pet, the value of pet_ids is just one id. Here's what owner_params look like after I create a new owner "Monica" and give her a new pet "Pearl":

<ActionController::Parameters {"name"=>"Monica", "pet_ids"=>"1"} permitted: true>

When these params get passed into the create method, pet_ids= is called on my new owner instance, which updates the pets table by assigning the owner_id to the selected pet, and the association is created.
(Also, if any of those pets had previous owners, they've been stolen. A pet can only belong to one owner!)

Selecting Multiple Options

What happens when I have the option to select multiple pets for my new owner? I can do this with collection_check_boxes or collection_radio_buttons (the arguments that these helpers take are identical to the ones for collection_select), or I can add an html option to my collection_select, like this:

<%= f.collection_select :pet_ids, Pet.all, :id, :name, {}, {multiple: true} %>

The form this generates is pretty ugly, but it gets the job done. To actually select multiple options, the Command or Shift key has to be used.

Ugly Way to Select Multiple Options
An ugly form.

In this case, my method stays the same: I still call pet_ids on an owner instance no matter how many pets the owner is associated with. But the generated html looks different, and hints at the next step:

<select multiple="multiple" name="owner[pet_ids][]" id="owner_pet_ids">

In the allowed parameters, I now have to account for the multiple pet option:

def owner_params
    params.require(:owner).permit(:name, pet_ids: [])
end

Pet_ids= will accept an array of ids and update each pet row accordingly.

Form helpers are just another example of Rails magic happening behind the scenes, and they work great- just as long as you remember when to pluralize!

Posted on by:

dianakw8591 profile

dianakw8591

@dianakw8591

Rock climber turned coder.

Discussion

markdown guide