In the last few lessons, we've covered adding full CRUD functionality for albums to our record store. In this lesson, we'll do the same for adding CRUD functionality for songs. This time, we'll go back and forth between controller actions and views. Much of the code will be the same but there will be a few new wrinkles. These key differences will be highlighted in bold.
Because our application has a one-to-many relationship between albums and songs, it doesn't make much sense to have a separate index
action for showing every song. Instead, we'll just show the songs that belong to a specific album. This means that we can add this code to the show view of an album. Let's update that file now:
<h1><%= @album.name %></h1>
<h3><%= @album.genre %></h3>
<% if @album.songs.any? %>
<h4>Here are the songs for this album:</h4>
<% @album.songs.each do |song| %>
<ul>
<li>
<%= link_to song.name, album_song_path(@album, song) %>
</li>
</ul>
<% end %>
<% else %>
<p>No songs are listed for this album.</p>
<% end %>
<p><%= link_to "Add a song", new_album_song_path(@album) %></p>
<p><%= link_to "Edit", edit_album_path(@album) %></p>
<p><%= link_to "Delete album", album_path(@album),
:data => {:confirm => "Are you sure?",
:method => "delete"} %></p>
<p><%= link_to "Return to albums", albums_path %></p>
All the code should look familiar except for one key difference. Note that album_song_path
takes two arguments, not one: link_to song.name, album_song_path(@album, song)
. That's because in order to access this path, our application needs both an album's id
and a song's id
. We pass in the arguments in the same order that they are listed in the path prefix. Our application can handle the rest.
Next, let's create the controller action for adding a new song:
class SongsController < ApplicationController
def new
@album = Album.find(params[:album_id])
@song = @album.songs.new
render :new
end
end
There are two key differences here. First, we pass in album_id
in as a parameter. If we look at rake routes
, we'll see that the URI Pattern for the new_album_song_path
is /albums/:album_id/songs/new
. Note that :album_id
is the parameter here, not :id
. We could also verify the parameters by using binding.pry
as well.
The second key difference is that when we instantiate a new Song
, we specify @album.songs.new
, not Song.new
. This way, we can automatically associate the Song
instance with the correct Album
instance. Other than that, this should look familiar. First, we find the correct album based on the @album
passed into the album_song_path
. Then we instantiate a new Song
and render the corresponding new view.
Next, let's add the view. We'll need a new subdirectory called songs inside of views:
<h1>Add a new song:</h1>
<%= form_for [@album, @song] do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :lyrics %>
<%= f.text_area :lyrics %>
<p><%= f.submit %></p>
<% end %>
The key difference here is that we have to pass in two objects to form_for
instead of just one. Note that we use square brackets to do this. We pass in two objects because our application needs both a song and the album the song will belong to. We also use a text_area
instead of a text_field
for lyrics so there is more space in the field.
Next, we need a create
action in the SongsController
:
...
def create
@album = Album.find(params[:album_id])
@song = @album.songs.new(song_params)
if @song.save
redirect_to album_path(@album)
else
render :new
end
end
# Other controller actions go here.
private
def song_params
params.require(:song).permit(:name, :lyrics)
end
...
First, we find an instance of Album
based on the album_id
property in the params hash. Next, we create a new Song
and make sure it's associated with an instance of Album
by calling @album.songs.new
. Once again, we need to use a params
method so we have strong parameters. If the song is properly saved, we'll redirect back to the album the song belongs to.
The controller action for show
is straightforward. We'll just grab a few id
parameters and pass them into instance variables:
def show
@album = Album.find(params[:album_id])
@song = Song.find(params[:id])
render :show
end
We can then use these instance variables to populate our view for showing a specific song. This view includes links to edit and delete the song:
<h1>Song: <%= @song.name %></h1>
<h2>Album: <%= @album.name %></h2>
<h3><%= @song.lyrics %></h3>
<p><%= link_to "Edit song", edit_album_song_path(@album, @song) %></p>
<p><%= link_to "Delete song", album_song_path(@album, @song),
:data => {:confirm => "Are you sure?",
:method => "delete"} %></p>
<p><%= link_to "Return to album", album_path(@album) %></p>
Note that we're passing two instance variables into our link_to
helper methods. This is expected. We only need one for the album_path
, though. The prefix makes it clear which objects need to be passed into the path.
Next, let's add a controller action for the edit view:
def edit
@album = Album.find(params[:album_id])
@song = Song.find(params[:id])
render :edit
end
This is straightforward. Once again we need to find the Album
instance based on the album_id
parameter. Meanwhile, the Song
instance is based on the id
parameter. This can be verified via rake routes
. Finally, we render the edit view.
Our edit view looks pretty much exactly the same as our new view:
<h1>Edit song:</h1>
<%= form_for [@album, @song] do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.label :lyrics %>
<%= f.text_area :lyrics %>
<p><%= f.submit %></p>
<% end %>
Here's the update
action in our controller:
def update
@song = Song.find(params[:id])
if @song.update(song_params)
redirect_to album_path(@song.album)
else
@album = Album.find(params[:album_id])
render :edit
end
end
The code should look familiar now. In this case, we don't need to grab information about a specific album if the update is successful. We can just update the song itself. We can also pass in @song.album
as an argument to album_path
. This would also ensure that users would be redirected to the right album if we were to add functionality that allows users to associate a song with a different album. If the update fails, it will render the edit view instead. However, remember that the edit view requires @album
to be defined, so the form can correctly render. If @album
is undefined, a failing update
will crash your application.
Finally, here's the code for the destroy
action:
def destroy
@song = Song.find(params[:id])
@song.destroy
redirect_to album_path(@song.album)
end
This should look familiar, too. We just need to find the song, not a specific album. Even after the song is destroyed, we still have access to its properties, which means we can still pass @song.album
as an argument to album_path
.
We've now included full CRUD functionality for songs. Go to the root directory of the project, type in rails s
, and navigate to localhost:3000 in the browser to see the changes to our application.
Before we move on, let's recap the key differences between adding CRUD functionality for a nested object versus adding CRUD functionality for a parent object:
album_song_path(@album, @song)
takes two arguments while album_path(@album)
only takes one.:id
while the parent object has a key of :<name_of_parent_class>_id
. Since the parent object is an album, that means the key will be :album_id
. We can always verify this by checking rake routes
and looking at the value of the URI for a specific route.@album.songs.new
. We call #new
on @album.songs
, not @song
. Contrast this with the code we'd write for adding a new album: @album.new
.form_for [@album, @song]
for a nested song. Meanwhile, we only need to pass in an album for the parent object's form helper: form_for @album
.
Lesson 12 of 34
Last updated July 14, 2022