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.
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!
Top comments (3)
Nice post
Thank you!
thank you so much, I was struggling with this stupid helper for hours and couldn't find the solution to my problem and it looks like using singular id really was the problem