form_with and Collection Select with Nested Routes
In the age of Covid-19, there has been a significant increase in the use of social media. Folks are social distancing — how else will we keep in touch with our loved ones?
Unfortunately, social media could transform from a wonderful tool for connection into a very unhealthy obsession if it is not used in moderation. That’s why, for my Flatiron Ruby on Rails project, I have created Social Media Distancing — a goal-setting app that helps users switch their focus from the glamorized lives of others to their own improvement.
Let’s start with the form. Since form_with
and form_tag
are soft-deprecated, I used form_with
.
<%= form_with model: ([@journey.user, @journey]), local: true do |f| %> <%= f.label :title %> <%= f.text_field :title %> <%= f.label :content %> <%= f.text_area :content %> <%= f.label :goal %> <%= f.collection_select :goal_id, @goals, :id, :name, @journey.goal ? { selected: @journey.goal.id } : { include_blank: true } %> <%= f.submit %><% end %>
There is a bit going on here..so what is all of this? Take a look at the first line. As shown above, form_with
takes in a few arguments. I used :model
and :local
, but you can check out more options here.
The model specifies which object we would like to build the form on. Since my routes are nested, my arguments look a little different.
<%= form_with model: ([@journey.user, @journey]), local: true do |f| %>
It’s an array! I’m telling form_with
that I would like to build on a@journey
(defined in JourneysController) that is nested in the path of the user that the journey belongs to (@journey.user
). This outputs the following HTML:
<form action="/users/2/journeys">
...
</form>
Great! Looks like the form is posting to the expected route.
The next argument I used was :local
. To keep my code dry, I am creating this form as a partial and rendering it with locals. Unlike form_for
and form_tag
, form_with
is remote, requiring me to manually specify that local: true
.
Last of line 1 is its block, do |f|
. form_with
yields a form builder object that we call each field on.
<%= f.label :title %><%= f.text_field :title %><%= f.label :content %><%= f.text_area :content %><%= f.label :goal %>
Onto the collection_select
field.
<%= f.collection_select :goal_id, @goals, :id, :name, @journey.goal ? { selected: @journey.goal.id } : { include_blank: true } %>
This will create a dropdown for users to choose a goal from. Here, you can see that I have passed in four arguments. the first, :goal_id
, takes the value of the form, names it so it could be used as the correct parameter, and passes it back to the controller.
The second argument, @goals
, specifies which collection we would like to choose from. In this case, it is a list of all of the goals available.
The next two, :id
and :name
label our options in a way. The :id
argument is the value of each option, and the :name
argument is what the user sees from the list they are choosing from.
Lastly, I have a :selected
argument. Since this form is a partial, I have passed in a conditional as the value.
@journey.goal ? { selected: @journey.goal.id } : { include_blank: true }
All this says is, “if the journey has been assigned a goal (i.e. it is an existing journey that is being edited), show its goal on the select bar. Otherwise (if it is a new, unsaved journey), leave the select bar blank.
That’s it! Creating forms with nested routes does not have to be hard. It may be confusing initially, but once you understand all of the arguments, it is smooth sailing from there. Now that I’ve figured out how to do that, it is time for me to go and social media distance. Happy coding!