Routes: Linking Pages

Exercises

Objective

The last exercise was a bit of a cliffhanger. We ended up with two different pages, but no way to easily navigate between them. That's not very web-worthy. In this exercise we'll add links so we can navigate between those pages. You know, like the World Wide Web was designed to work!

1. Link to Index Page

First, we want an "All Movies" link in the header that takes us back to the index page where all the movies are listed.

To do that, we could just hard-wire a link to /movies, like so:

<a href="/movies">All Movies</a>

That would work, but it makes our application really brittle because we would have link information in two places. First, we have the information in our routes file where we've told it how to handle /movies. And secondly, we're explicitly typing out the URL here, and assuming there's a matching route. So, if we decide later to change the URL, we'll need to make changes in two places. And that's always a bad sign.

So instead of hard-wiring the URL, we'll ask the router to generate the URL for us. That way the link will always have a matching route that points back into our application. Better yet, if we want to change what the URL looks like down the road, we can do that in one place: the routes file.

  1. Start by navigating to http://localhost:3000/rails/info/routes to see all the routes we've defined so far.

    You should see a list of routes represented in a table, something like this (we'll ignore the other default routes):

    Helper        HTTP Verb        Path               Controller#Action
    movies_path     GET        /movies(.:format)      movies#index
                    GET        /movies/:id(.:format)  movies#show
    

    What we have here is a handy inventory of the requests that the router recognizes and the matching controller/action pair to handle the request. It's the same thing we have in the config/routes.rb file, but it's easier to see here what's actually going on.

    The first row is the route that we want to be matched when we click the "All Movies" link, so let's take a closer look at it:

    movies_path  GET /movies(.:format)     movies#index
    • The first column contains the name of the route helper method used to generate a URL that's matched (recognized) by this route. Notice the method name includes the plural form movies because this route deals with multiple movies.

    • The second column is the HTTP verb, which is a GET in this case. This is the most common HTTP verb because you typically "get" web pages, but we'll see other verbs a bit later.

    • The third column is the path. You can think of it as a pattern that's used to match against the requested URL. Here, the pattern is a literal /movies, followed by an optional format in parentheses. (Don't worry about the format.)

    • The last column is the controller/action pair that will handle the route. We often say that the request is "mapped" to the controller/action pair.

    So this route will match a GET request for the URL http://localhost:3000/movies. Remember, the http://localhost:3000 part of the URL identifies the web server and it then forwards the /movies part on to the router. The request then gets dispatched to the index action of the MoviesController.

  2. So, how can this help us? Well, in addition to being able to map requests based on defined routes, the router can also generate URLs that match these routes. And to generate a URL, we simply call the appropriate route helper method. For example, to generate a link to the movie listing page, Rails gives us two possible methods to choose from:

    helper method generates
    movies_path /movies
    movies_url http://www.example.com/movies

    Notice that the _path method generates the path part of the URL and the _url method generates the full URL. In practice you want to use the _path variant in view templates and the _url variant in controllers as redirects (which we'll learn about later) require fully-formed URLs to be technically valid.

  3. So now we know how to generate the URL that leads back to the movie listing page. The next step is to actually generate a hyperlink. And Rails has a helper method for doing that, too. The link_to helper is a method that takes two parameters:

    • The first parameter is the text to link, such "All Movies".

    • The second parameter is the destination URL (or path), such as the URL path returned from the movies_path method.

    So with that in mind, we're ready to add an "All Movies" link in the header. To take advantage of styling, in the app/views/layouts/application.html.erb layout file change the header to include a nav and an empty list of links, like so:

    <header>
      <nav>
        <%= image_tag("logo") %>
        <ul>
          <li>
            # link goes here
          </li>
        </ul>
      </nav>
    </header>
    

    Then generate an "All Movies" link that points back to the index page.

    Call the link_to helper with two parameters: "All Movies" and the result of calling the movies_path route helper. Remember that parentheses are optional when calling methods in Ruby, so you can leave off the parentheses if you prefer.

    <%= link_to("All Movies", movies_path) %>
    
  4. Then refresh the show page and you should see the link. View the page source and you should see that the link points to the same URL we avoided hard-wiring.

    <a href="/movies">All Movies</a>
  5. Finally, enjoy the thrill of clicking that link! It's ok if you act totally surprised when you see the movie listing.

2. Link Index Page to Show Page

Next, let's reciprocate that link love. We want each movie title on the index page to be a link to the movie's show page. This is very similar to what we just did, but there's a slight twist...

  1. Check out the list of defined routes again:

    Helper        HTTP Verb        Path               Controller#Action
    movies_path     GET        /movies(.:format)      movies#index
                    GET        /movies/:id(.:format)  movies#show
    

    This time we want to generate a URL that gets routed to the show action, so the second row will be the matching route. But which route helper method should we call to generate the URL? Well, there is no helper in the leftmost column of the route we're interested in. So to get a helper method we need to give the route a name.

  2. In the config/routes.rb file, give the route for showing a movie the name movie using the as: option. By convention, the name of the route that maps to the show action should be singular because we're dealing with one thing.

    Show Answer

    Hide Answer

    get "movies/:id" => "movies#show", as: "movie"
    
  3. Then refresh http://localhost:3000/rails/info/routes and in the leftmost column you should now have helpers for both routes:

    Helper        HTTP Verb        Path               Controller#Action
    movies_path     GET        /movies(.:format)      movies#index
    movie_path      GET        /movies/:id(.:format)  movies#show
    

    The second route now has a helper named movie_path. Be mindful here of the subtle difference between the two helpers in the leftmost column: one includes the plural form movies and the other includes the singular form movie. It's really easy to get these confused! Just remember that the plural-form helper is used when dealing with multiple movies and the singular-form helper is used when dealing with one specific movie.

  4. Now we're ready to actually generate the links to the show page! Keep a couple things in mind:

    • The first parameter to link_to will be the movie's title.

    • The second parameter will be the result of calling the movie_path helper method. Remember, because the route for that helper has an :id placeholder, you must pass the movie's id as a parameter so that the :id placeholder will get assigned. As a shortcut, if you instead pass the movie object the router will assume that it should use the object's id attribute to fill in the :id placeholder.

    OK, go ahead and update the index template so that the movie title is a link to the movie's show page.

    Show Answer

    Hide Answer

    <%= link_to(movie.title, movie_path(movie)) %>
    
  5. Then refresh the index page and you should see links for each movie title. View the page source and notice that each link points to a distinct movie, like so:

    <a href="/movies/1">Avengers: Endgame</a>
    <a href="/movies/2">Captain Marvel</a>
    <a href="/movies/3">Black Panther</a>
  6. As a small reward for getting that working, here's another shortcut. When generating links to an object's show page, you can pass that object as the second parameter to link_to, like this:

    <%= link_to movie.title, movie %>
    

    In this particular case, you don't have to call the route helper method. Given the movie object, the link_to method assumes that it should call the movie_path method for you. It's a small bit of syntactic sugar that makes the code more compact and readable.

  7. Finally, have you experienced the thrill of clicking any of those links yet? By all means, click away! And be sure to check that all your links work before moving on.

3. Add a Root Route

Now that you're feeling like a routing and linking pro, let's fix something that's been annoying us from the get-go.

If you navigate to http://localhost:3000, you'll see the default Rails welcome page. It would be a lot more useful if browsing to that URL always showed our movie listing, instead of the default welcome page. To do that, we can define something called a root route.

  1. First, in the config/routes.rb file, add a root route that maps to the index action of the MoviesController.

    Show Answer

    Hide Answer

    root "movies#index"
    
  2. Now browse to http://localhost:3000 and you should see the movie listing. Hurray!

  3. Next, we might as well change the logo image in the header to link to http://localhost:3000. Do we have a route helper method available to generate that URL? Let's look at the defined routes:

    Helper        HTTP Verb        Path               Controller#Action
    root_path       GET        /                      movies#index
    movies_path     GET        /movies(.:format)      movies#index
    movie_path      GET        /movies/:id(.:format)  movies#show
    

    What's the name of the helper method that generates the URL? Using that helper, change the logo image in the header to point back to the root URL.

    Show Answer

    Hide Answer

    <%= link_to image_tag("logo"), root_path %>
    

Solution

The full solution for this exercise is in the link-pages directory of the code bundle.

Bonus Round

Displaying Routes

You can also print out a list of all the defined routes by going to a command prompt and typing:

rails routes

You should see a list like this (we'll ignore the other default routes):

Prefix   Verb   URI Pattern             Controller#Action
movies   GET    /movies(.:format)       movies#index
movie    GET    /movies/:id(.:format)   movies#show

The output is slightly different than listing the routes by navigating to http://localhost:3000/rails/info/routes. Instead of the leftmost column containing the name of a helper method, it just shows a prefix. For example, the route in the first row has the prefix movies. The trick to deciphering this is knowing that the name of the helper method is derived from the prefix of the route. In this case the route prefix is movies, and therefore the helper methods are named movies_path and movies_url.

Trying Route Helpers in the Console

You can experiment with route helper methods in a Rails console session using the special app object. For example, here's how to try the movies_path route helper from inside a Rails console session:

>> app.movies_path
=> "/movies"

Notice it generates the path part of the URL.

To generate the full URL, you need to use the movies_url helper, like so:

>> app.movies_url
=> "http://www.example.com/movies"

To generate the path to a specific event, you use the movie_path helper and either pass it a movie's id or (as a shortcut) just the movie:

>> movie = Movie.find(1)

>> app.movie_path(movie.id)
=> "/movies/1"

>> app.movie_path(movie)
=> "/movies/1"

And to generate a link to the "root" page, you use the root_path helper:

>> app.root_path
=> "/"

Pop Quiz

Sharpen your pencil, it's time for a pop quiz! Let's say that you are building a Rails app with books for sale, and that your routes so far look like this:

books_path  GET /books(.:format)      books#index
            GET /books/:id(.:format)  books#show
  1. What URL will calling the books_path method generate?

    Show Answer

    Hide Answer

    The _path route method generates a relative path, in this case /books

  2. What URL will calling the books_url method generate?

    Show Answer

    Hide Answer

    The _url route method generates the full URL, in this case http://www.example.com/books

  3. The link_to helper method takes 2 parameters:
    the name of the __________________, and the ______________________.

    Show Answer

    Hide Answer

    The link_to helper method takes 2 parameters: the text to link (such as "All Books"), and the destination URL (or path).

  4. How would you link the words "See All Books" to the index page of the books app?

    Show Answer

    Hide Answer

    <%= link_to("See All Books", books_path) %>

    or

    <%= link_to "See All Books", books_path %>

  5. By convention, the name of the index route is books. What would you name the route for showing a particular book?

    Show Answer

    Hide Answer

    The name of the route would be book because, by convention, the name of the route that maps to the show action should be singular since we're dealing with one thing.

  6. How would you link a book's title to the book's show page? There are 2 possible answers.

    Show Answer

    Hide Answer

    <%= link_to(book.title, book_path(book)) %>

    or

    <%= link_to(book.title, book) %>

    You don't have to call the route helper method. Given the book object, the link_to method assumes that it should call the _path method for you. But remember, this shortcut only works for generating links to an object's show page.

  7. How would you add a root route that maps http://localhost:3000 to the index page?

    Show Answer

    Hide Answer

    root "books#index"
    
  8. Lastly, with our root route in place, how would you change the "See All Books" link on the show page to now point to the root URL.

    Show Answer

    Hide Answer

    <%= link_to("See All Books", root_path) %>
    

Nicely done! Routes can be a little tricky to wrap your head around, so have patience with yourself as you work with them. In time, they'll become as familiar as the route to your favorite coffee shop. (We never promised our jokes would be funny.)

Wrap Up

The router is a super powerful part of Rails. It also tends to be one of the most challenging, particularly because there are so many conventions at work behind the scenes. When you're in doubt, the best thing to do is follow the routes. Look at the defined routes to see what's going on, identify the name of the route you're interested in, and then play with the route helper methods in the console until you're confident. Oh, you think that's what we tell all the beginners? Actually, no. That's exactly how we debug routing problems!

Now that we have a full MVC stack with multiple pages linked together, in the next section we'll start adding pages with web forms. You know, so we can edit and create movies from the comfort of our browser!