Trouble in params-idise

A brief nautically-themed primer on the use of collection_check_boxes for keeping track of serialized object attributes

Let’s hope we don’t get ship-rekt

Let’s say you are (as I was recently) trying to keep track of two classes that share a has_many:, through: relationship through a join table. If you were trying to keep track of ships and the ports at which they make land, for instance, your models might look something like this:

class Ship < ApplicationRecord
has_many :ship_ports
has_many :ports, through: :ship_ports
end
class Port < ApplicationRecord
has_many :ship_ports
has_many :ports, through: :ship_ports
end
class ShipPort < ApplicationRecord
belongs_to :ship
belongs_to :port
end

Furthermore, let’s say that, on creation of a new ship, you would like to display a list of available ports at which the new ship can call, so that each port knows which ships to expect and each ship knows which ports will have space for it. Something like this:

What a wide selection of ports.

Well, you could always rely on the tried-and-trusty check_box_tag or check_box_form, right? But that’s boring. And you’re here because you’re not boring. Or because you know you’re supposed to use check_box to pass a test, but you find the syntax confusing so you really want to use check_box_tag, but you know you’re not supposed to, so you want to see if you can bend the rules a little bit. Can you bend the rules a little bit? Of course you can. It’s programming. So let’s do something interesting. I give you: collection_check_boxes

# In our new ship form view:
# /app/views/ships/new.html.erb
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Create a ship here!</title>
</head>
<body>
<h3>Create a new ship here:</h3>
<%= form_for @ship do |f| %>
<%= f.text_field :name %><br>
<%= f.collection_check_boxes :port_ids, Port.all, :id, :name
do |t| %>
<%= t.check_box %><%= t.label %>
<% end %>
<%= f.submit %>
<% end %>
</body>
</html>

Most of this is business as usual if you’re accustomed to making forms, so let’s zoom in on the juicy bit — the form itself.

<%= form_for @ship do |f| %>
<%= f.text_field :name %>
<%= f.collection_check_boxes :port_ids, Port.all, :id, :name do |t| %>
<%= t.check_box%><%= t.label %>
<% end %>
<%= f.submit %>
<% end %>

What’s that funky f.collection_check_boxes line? Well, if you’ve made it this far, you’re probably familiar with form_for. Here, we are calling the collection_check_boxes Rails helper method on our FormBuilder object (f). In this example, it’s taking a bunch of arguments. What are they? Let’s go through each in the correct syntactical order.

  • :port_ids — This will become a key within our params[:ship] hash. Its values will be an array of the ids of the Ports that were selected on creation of a new ship. Note that the values will be stored as strings, but these are easily converted to integer form.
  • Port.all — Here we specify the collection objects for which we will be creating checkboxes. In this case, we want one checkbox for each port, so we can simply call Port.all.
  • :id — This third argument is the method that we would like to call on each of the items for which we have made a checkbox (here, all ports, or Port.all). The return value of this method, as called on each object for which we have made a checkbox, will be passed in to our params[:ship][:port_ids] array if the box is checked. Thus, we are here simply calling the id method on each Port object that was checked in the form, and then passing it into our params.
  • :name — The final argument is a text method to be called on each object in our collection of ports. The return value of this method (here, the name of each port checked off) can then simply be called using label.

So all together, we have:

<%= f.collection_check_boxes :port_ids, Port.all, :id, :name do |t| %>

Note that we are again iterating over collection_check_box objects (do |t|), so don’t forget to close your form with an additional <% end %>.

So what’s next? This:

<%= t.check_box%><%= t.label %>

Pretty self-explanatory from here, right? We are simply creating a check box and label for each Port in our Port.all collection.

Where does this get us, if not Havana, New York, or San Diego? Well, our params will look something like this (if we only checked off New York, which has an id of 2, and named our ship "H.M.S. Surprise”):

params = {"utf8"=>"✓", "authenticity_token"=>"JMg2hkSa/eQJkvDnT07NRx9AqimHDWxJNK0bO2MgKjcKJzBe2cTrRV21LwuQYOFuJO67yc4JMN3naVSkKO9G4g==", "ship"=>{"name"=>"H.M.S. Surprise", "port_ids"=>["2", ""]}, "commit"=>"Create Ship", "controller"=>"ships", "action"=>"show_params"}

Again highlighting the juicy bits:

params = {
...
"ship"=>{"name"=>"H.M.S. Surprise", "port_ids"=>["2", ""]},
...
}

It worked! Simply reject the extraneous "" in your port_ids array, convert each element in the array to an integer, and you’re ready to find and associate your ports with your ships!