Beginners Tutorial: Routing in Rails 2.0 (with REST) – Part 4 of n

Part 5

The routes we have discussed so far are known as simple routes. I have been referring to them as custom routes, but ‘simple routes’ is the correct term. The default route we discussed in part 3 is also a simple route. The aim is to move onto RESTful routes, sometimes called resource routes. RESTful routes are built on top of named routes. Therefore we must first cover named routes, and that is what part 4 is all about.

Part 4

Introduction

Referring back to part 1, the routing system has two main functions:
Interpreting a request URL
Generating a request URL

We had a detailed look at route generation and interpretation in part 2, but let’s have another quick look at it. Let’s assume, for some insane reason, that we defined the following route in the music_store routes.rb file:

map.connect ‘apples/bananas/:id’, :controller => “albums”, :action => “show”

link_to would use this rule to generate a URL, this can be seen in the following line extracted from index.html.erb:

link_to ‘Show’, :controller => “albums”, :action => “show”, :id => album.id

This could generate a URL of: http://localhost:3000/apples/bananas/7
And when given this request, Rails would handle it correctly e.g. the show action will be called on the albums controller and the show view will be sent as the response to the browser.

Coming back to the real world, we are more likely to define the routing rule like this:
map.connect ‘albums/show/:id’, :controller => “albums”, :action => “show”

link_to (and friends) would then generate a URL like this:
http://localhost:3000/albums/show/7.

To show the index (the listing of the albums) we could define a rule like this:
map.connect ‘albums/index’, :controller => “albums”, :action => “index”

The matching link_to in edit.html.erb is:
link_to ‘Back’, :controller => “albums”, :action => “index”

Looking at the last example, there is some repetition. Both lines contain:
:controller => “albums”, :action => “index”

The link_to method needs this (controller, action) information to obtain the URL. If we had the URL stored as some global variable, or method, then we could simply pass that into link_to. link_to could use this ‘literal’ URL without needing to generate one.
This could look something like: link_to ‘Back’, albums_index_url
This is one of the main ideas behind named routes.

Named Routes

Named routes provide a way to ‘clean up’ code, specifically the code within link_to and other similar functions. When a named route is defined, new helper methods are dynamically generated. These helper methods provide a simplified way of obtaining the required request URL. The names of these helper methods are based on the name of the route. Let’s clarify this by coding.

Experiment 4.0 Preparation

At the end of part 3 the routes.rb had the default route in it:

ActionController::Routing::Routes.draw do |map|  

  map.connect ':controller/:action/:id'

end

It is common to accompany the default route with this:
map.connect ‘:controller/:action/:id.:format’
We can think of this as a default route which allows clients to request a different format for the response. :format was discussed in part 1, under the title of Experimenting 1 – View the generated routes.

To prepare for this session, we want to remove all the routes and completely break the site. Therefore you should make your routes.rb look like this:

ActionController::Routing::Routes.draw do |map|  

  # map.connect ':controller/:action/:id'
  # map.connect ':controller/:action/:id.:format'

end

Now the site is good for two things, serving up routing errors and learning about named routes…

Experiment 4.1 Adding a Named Route for the Albums Index

In part 2 we defined a route which invoked the index action on the albums controller. That route looked like this: map.connect ‘music’, :controller => “albums”, :action => “index”

To follow Rails convention, we should write it like this:
map.connect ‘albums’, :controller => “albums”, :action => “index”

Making it more readable we could write it like this:
map.connect ‘albums’,
:controller => “albums”,
:action => “index”

The pattern string is ‘albums’, therefore a URL of /albums would match.

In order to name this route, we need to decide what to name or call it. A top down approach is recommended for this. We want to show a list of albums, so it seems reasonable to call the route ‘albums’.

To name the route, we make a small change to our route definition. We change the route definition from:

map.connect ‘albums’,
:controller => “albums”,
:action => “index”

To:

map.albums ‘albums’,
:controller => “albums”,
:action => “index”

All we had to do is change map.connect to map.albums. Modify your routes.rb file to look like this:

  ActionController::Routing::Routes.draw do |map|  

    map.albums 'albums',
        :controller => "albums",
        :action => "index"

    # map.connect ':controller/:action/:id'
    # map.connect ':controller/:action/:id.:format'

  end

Once this file is saved, you will have created a named route. We are still missing most of the routes our site requires therefore we can’t run the site just yet. Let’s simplify the index page. Make the index.html.erb look like this:

<h1>Listing albums</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Review</th>
  </tr>

<% for album in @albums %>
  <tr>
    <td><%=h album.title %></td>
    <td><%=h album.review %></td>
  </tr>
<% end %>
</table>
<br />

Now you should be able to see the simplified index page. Next we will take a look at the methods that Rails generates when using named routes.

Experiment 4.2 Exploring the ‘Named Route’ Generated Methods

When a named route is defined, Rails will dynamically generate two methods, these are:
name_url and name_path (where name is the name of the route). name_url will return the fully qualified URL, name_path returns the relative path – it does not include the protocol and host.

In our case, we have a route named albums. When we run the site, rails will generate albums_url and albums_path. Let’s make the view display these values, change the index.html.erb to this:
I had to use different formatting to stop WordPress from getting up to its shenanigans…

<h1>Listing albums</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Review</th>
  </tr>

<% for album in @albums %>
  <tr>
    <td><%=h album.title %></td>
    <td><%=h album.review %></td>
  </tr>
<% end %>
</table>
<br />
albums_url: <%= albums_url %>
<br />
albums_path: <%= albums_path %>

Brows to http://localhost:3000/albums and you should see:
albums_url: http://localhost:3000/albums
albums_path: /albums

Now that we have seen the output of these methods, you can now remove those last three lines.

Academically speaking it is probably more correct to use name_url but name_path also works in most cases. The Rails way is to use name_path if you can. These methods are available in the views and controllers.

Experiment 4.3 Defining a Named Route for Show

Showing the index is fairly straight forward as it is basically a static URL. Now we look at defining a named route for show. Show needs to pass through the ID of the specific album. When defining a simple route, we could use:

map.connect ‘album/:id’,
:controller => “albums”,
:action => “show”

Once again, we can convert this into a named route by replacing connect with the name of the route. Let’s name the route ‘album’. Update the routes.rb to look as follows:

ActionController::Routing::Routes.draw do |map|  

  map.albums 'albums',
      :controller => "albums",
      :action => "index"

  map.album 'album/:id',
      :controller => "albums",
      :action => "show"

    # map.connect ':controller/:action/:id'
    # map.connect ':controller/:action/:id.:format'

end

Rails realises that it needs a value for :id, therefore the helper methods accept a parameter to be assigned to it. Therefore link_to can use album_url(3). This will hard code 3, but it illustrates the point.

In the code we are likely to use:
link_to ‘Show’, album_path(album.id)
Thanks to some syntactic sugar we can also use:
link_to ‘Show’, album_path(album)

Let’s get the ‘show’ functionality working again. Update index.erb.html to look like this:

<h1>Listing albums</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Review</th>
  </tr>

<% for album in @albums %>
  <tr>
    <td><%=h album.title %></td>
    <td><%=h album.review %></td>
	<td><%= link_to 'Show', album_path(album)  %></td>
  </tr>
<% end %>
</table>
<br />

And update show.erb.html to:

<p>
  <b>Title:</b>
  <%=h @album.title %>
</p>

<p>
  <b>Review:</b>
  <%=h @album.review %>
</p>

<%= link_to 'Back', albums_path %>

We have removed the link to ‘edit’, we will replace it later.

At this stage the site should be able:

  • to show the list of albums in the index page
  • show a single album

Experiment 4.4 Defining a Named Route for Edit

Now lets fix the edit functionality. Update routes.rb to look like this:

ActionController::Routing::Routes.draw do |map|  

  map.albums 'albums',
    :controller => "albums",
    :action => "index"

  map.album 'album/:id',
    :controller => "albums",
    :action => "show"

  map.edit_album 'albums/:id/edit',
    :controller => "albums",
    :action => "edit"

  map.update_album 'albums/:id/update',
    :controller => "albums",
    :action => "update"  

    # map.connect ':controller/:action/:id'
    # map.connect ':controller/:action/:id.:format'

end
 

You may have noticed that I am using slightly different pattern strings to what we have been using. The pattern strings (and URLs) that I am using now are fairly close to those used in REST. Therefore we are already getting familiar with the RESTful interface.

Update the index.erb.html to:


<h1>Listing albums</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Review</th>
  </tr>

<% for album in @albums %>
  <tr>
    <td><%=h album.title %></td>
    <td><%=h album.review %></td>
    <td><%= link_to 'Show', album_path(album)  %></td>
    <td><%= link_to 'Edit', edit_album_path(album) %></td>
  </tr>
<% end %>
</table>
<br />

 

Edit.erb.html should look like this:

<h1>Editing album</h1>

<%= error_messages_for :album %>

<% form_for (@album), :url => update_album_path(@album) do |f| %>
  <p>
    <b>Title</b><br />
    <%= f.text_field :title %>
  </p>

  <p>
    <b>Review</b><br />
    <%= f.text_area :review %>
  </p>

  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>

<%= link_to 'Show', album_path(@album) %> |
<%= link_to 'Back', albums_path %>

Add the edit link back to show.erb.html

  <p>
  <b>Title:</b>
  <%=h @album.title %>
</p>

<p>
  <b>Review:</b>
  <%=h @album.review %>
</p>

<%= link_to 'Edit', edit_album_path(@album) %> |
<%= link_to 'Back', albums_path %>

At this stage the site should be able:

  • to show the list of albums in the index page
  • show a single album
  • Edit an existing album

Experiment 4.5 Defining a Named Route for Destroy

Update the routes.rb to this:

ActionController::Routing::Routes.draw do |map|  

  map.albums 'albums',
    :controller => "albums",
    :action => "index"

  map.album 'album/:id',
    :controller => "albums",
    :action => "show"

  map.edit_album 'albums/:id/edit',
    :controller => "albums",
    :action => "edit"

  map.update_album 'albums/:id/update',
    :controller => "albums",
    :action => "update"  

  map.destroy_album 'albums/:id/destroy',
    :controller => "albums",
    :action => "destroy"     

    # map.connect ':controller/:action/:id'
    # map.connect ':controller/:action/:id.:format'

end

The index.erb.html should look like this:

<h1>Listing albums</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Review</th>
  </tr>

<% for album in @albums %>
  <tr>
    <td><%=h album.title %></td>
    <td><%=h album.review %></td>
	<td><%= link_to 'Show', album_path(album)  %></td>
	<td><%= link_to 'Edit', edit_album_path(album) %></td>
	<td><%= link_to 'Destroy', destroy_album_path(album.id), :confirm => 'Are you sure?' %></td>
  </tr>
<% end %>
</table>
<br />

I am sure that you are getting a feel for named routes now…

At this stage the site should be able:

  • to show the list of albums in the index page
  • show a single album
  • Edit an existing album
  • Delete an album

Experiment 4.6 Defining a Named Route for New

Change the routes.rb file to this:

ActionController::Routing::Routes.draw do |map|  

  map.albums 'albums',
    :controller => "albums",
    :action => "index"

  map.album 'album/:id',
    :controller => "albums",
    :action => "show"

  map.edit_album 'albums/:id/edit',
    :controller => "albums",
    :action => "edit"

  map.update_album 'albums/:id/update',
    :controller => "albums",
    :action => "update"  

  map.destroy_album 'albums/:id/destroy',
    :controller => "albums",
    :action => "destroy"     

  map.new_album 'albums/new',
    :controller => "albums",
    :action => "new"  

  map.create_album 'albums/create',
    :controller => "albums",
    :action => "create"  

    # map.connect ':controller/:action/:id'
    # map.connect ':controller/:action/:id.:format'

end

The index.erb.html should now be:

<h1>Listing albums</h1>

<table>
  <tr>
    <th>Title</th>
    <th>Review</th>
  </tr>

<% for album in @albums %>
  <tr>
    <td><%=h album.title %></td>
    <td><%=h album.review %></td>
	<td><%= link_to 'Show', album_path(album)  %></td>
	<td><%= link_to 'Edit', edit_album_path(album) %></td>
	<td><%= link_to 'Destroy', destroy_album_path(album.id), :confirm => 'Are you sure?' %></td>
  </tr>
<% end %>
</table>
<br /><%= link_to 'New album', new_album_path %>

To stop WordPress from deleting my code, I put link_to ‘New album’  on the same line as the br tag. They are on separate lines in my actual code.

With new.erb.html looking like this:


<h1>New album</h1>

<%= error_messages_for :album %>

<% form_for :album, @album, :url => create_album_path do |f| %>
  <p>
    <b>Title</b><br />
    <%= f.text_field :title %>
  </p>

  <p>
    <b>Review</b><br />
    <%= f.text_area :review %>
  </p>

  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', albums_path %>

At this stage the site should be able:

  • to show the list of albums in the index page
  • show a single album
  • Edit an existing album
  • Add a new album

Are we done? What about the controller?

Experiment 4.7 Using Named Route Helper Methods in the Controller

Open the controller and look at the destroy action, you will notice this line:

format.html { redirect_to(:controller => “albums”, :action => “index”) }

This shows you that we can still generate routes as if we were using the simple routes. We have defined named routes, we can use the helper methods in the controller, so we might as well.

Make the controller look like this:

class AlbumsController < ApplicationController
  # GET /albums
  # GET /albums.xml
  def index
    @albums = Album.find(:all)
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @albums }
    end
  end

  # GET /albums/1
  # GET /albums/1.xml
  def show
    @album = Album.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @album }
    end
  end

  # GET /albums/new
  # GET /albums/new.xml
  def new
    @album = Album.new

    respond_to do |format|
      format.html # new.html.erb
      format.xml  { render :xml => @album }
    end
  end

  # GET /albums/1/edit
  def edit
    @album = Album.find(params[:id])
  end

  # POST /albums
  # POST /albums.xml
  def create
     @album = Album.new(params[:album])  

     respond_to do |format|
       if @album.save
         flash[:notice] = 'Album was successfully created.'
         format.html { redirect_to(album_path(@album)) }
         format.xml  { render :xml => @album, :status => :created, :location => @album }
       else
         format.html { render :action => "new" }
         format.xml  { render :xml => @album.errors, :status => :unprocessable_entity }
       end
     end
  end  

  # PUT /albums/1
  # PUT /albums/1.xml
  def update
     @album = Album.find(params[:id])  

     respond_to do |format|
       if @album.update_attributes(params[:album])
         flash[:notice] = 'Album was successfully updated.'
         format.html { redirect_to(album_path(@album)) }
         format.xml  { head :ok }
       else
         format.html { render :action => "edit" }
         format.xml  { render :xml => @album.errors, :status => :unprocessable_entity }
       end
     end
   end  

  # DELETE /albums/1
  # DELETE /albums/1.xml
  def destroy
    @album = Album.find(params[:id])
    @album.destroy

    respond_to do |format|
      format.html { redirect_to(albums_path) }
      format.xml  { head :ok }
    end
  end
end

Sure we have a larger routes.rb file, but I am sure you will agree that  redirect_to(albums_path)

is much better than redirect_to(:controller => “albums”, :action => “index”)

The End of Part 4

I hope this has helped you to get familiar with named routes. There is a lot more to them, and there is also a lot more syntactic sugar. Once I have completed the section on RESTful routing, I might do a summary post. This would be a recap and it will show some tips and tricks that I skipped over in these posts. I am skipping certain topics as I would like to get onto RESTful routing as soon as possible.

Speaking of RESTful routing, the way we have named the routes is very close to the RESTful way. You will see this in the next post.

It is very easy to make mistakes when writing a tutorial like this. Especially with the gremlins inside of the WordPress formatting engine. If you are one of the first to complete this session successfully then please inform me. More importantly, if you find a problem please let me know.

Thank you, Daryn

Part 5

20 thoughts on “Beginners Tutorial: Routing in Rails 2.0 (with REST) – Part 4 of n

  1. Pingback: Beginners Tutorial: Routing in Rails 2.0 (with REST) - Part 3 of n « YAB

  2. I have fixed the errors brought to my attention by Philip – thanks again.

    It turns out that WordPress does not always like the <%= tag. That is why the last two lines of index.erb.html in 4.6 are on the same line. If I separate them, WordPress eats the last line.

    Please let me know if you come across any problems…

    – Thanks Daryn

  3. Daryn,

    I’m back after having to do some other stuff – do you have an ETA for part 5? (No pressure!)

    Regards,

    Phil.

  4. Hi Phil, I did not work on part 5 this weekend. I needed to support the South African teams in the Super 14 – as an Australian, I am sure you understand 8)

    Pablo and Phil (and the others waiting for part 5),
    Part 5 should be out by next week Monday. My apologise for the delay. I plan on putting a fair amount of effort into it, so it should be worth the wait.

    – Daryn

  5. Hi Daryn,

    I’ve just followed your tutorial and thought it was nicely done. Shame about those formatting problems but the message got through all the same. Looking forward to the next installment.

    cheers,
    Tony.

  6. Daryn,

    At least we have one team in the semis – and you should have more time after next weekend!

    Regards,

    Phil.

  7. Back again after a diversion.

    This time I went back and actually did each tut and able to finish without any problems.
    Slowly getting the hang of it.

    Thanks and look fwd for the remainder…

    Tony

  8. Thanks a lot for these tutorials these have come in really hand in understanding just how routing in rails 2.0 works. So when should we expect Part 5?

  9. I am glad to help AikiGhost.

    I will do my best complete part 5 this weekend. If it is not out on Monday, I will give you a full refund.

  10. Pingback: Beginners Tutorial: Routing in Rails 2.0 (with REST) - Part 5 of n « YAB

  11. Pingback: This Week in Ruby (May 12, 2008) | Zen and the Art of Programming

  12. Excellent tutorial. I jumped into rails (and ruby) rather quickly and got lost a few times in the “magic”. Coming back to the basics definitely helps nail down a few basic concepts.
    Thanks again!

  13. Pingback: Tutorials in Routing in Rails 2.0 | ActivoRicordi Captain's log

  14. I need to do this in the edit view file:

    update_album_path(@album)) do |f| %>

    instead of this

    update_album_path(@album) do |f| %>

    I notice that’s different from your tutorial. Am I right or am I missing something?

    Thanks for the great explanations, btw!

    – Andy

  15. Hi Daryn,
    At first, i have to say your series of Routing tutorials is so great. I learned alot from that.
    At the end of Part 4, i found that there is a small problem with routes.rb file.

    There is nothing wrong with file routes.rb at the end of part 4 except the priority of routes. Let me say more detail: the 2nd priority of map.album is so high that it break other routes under it like: new, create. When we request /albums/new or /albums/edit, it will raise an error: Cannot find album with ID=new or ID=edit.

    The solution is quite straightforward: just fix the priority of routes. I recommend the routes order below, which works for me:

    map.albums ‘albums’, :controller => ‘albums’, :action => ‘index’
    map.new_album ‘albums/new’, :controller => ‘albums’, :action => ‘new’
    map.edit_album ‘albums/:id/edit’, :controller => ‘albums’, :action => ‘edit’
    map.create_album ‘albums/create’, :controller => ‘albums’, :action => ‘create’
    map.album ‘albums/:id’, :controller => ‘albums’, :action => ‘show’
    map.update_album ‘albums/:id/update’, :controller => ‘albums’, :action => ‘update’
    map.destroy_album ‘albums/:id/destroy’, :controller => ‘albums’, :action => ‘destroy’

    – Phan Anh Vu – http://cntt.tv

  16. Thanks so much for these tutorials. I had started Rails a few years ago with version 1.85 and when I came back to it I was totally lost. You have put back the sparkle… keep it coming.
    Thanks again.

Leave a comment