Views and Controllers

Exercises

Objective

Now that we've generated a skeleton application, it's time to start customizing it by adding features. The first feature we want to add is a page that lists movie titles. This is conventionally called the "index" page. The index page will be displayed when a user requests the URL http://localhost:3000/movies.

To make that work, we have three primary tasks:

  1. Add a route to handle requests for /movies

  2. Generate a MoviesController and define an index action that prepares an array of movie titles

  3. Create a view template (index.html.erb) that dynamically generates an HTML list of movie titles

Here's a visual of our objective:

We'll take this step-by-step, using the error messages as a guide along the way. So let's get started!

1. Display a List of Movie Titles

We'll start by displaying a hard-coded list of movie titles just to shake everything out before making things more dynamic.

  1. Start by making sure you have the flix app running in a Terminal or command prompt window:

    rails s

    If the app is already running on port 3000, then you'll get an "Address already in use" error.

  2. It's common to leave the server running in one window, and using a second Terminal or command prompt window to type in commands. So make sure you have a second command prompt ready to accept commands inside of the flix project directory.

  3. Then use your web browser to browse to http://localhost:3000/movies. You should get the following error message:

    Routing Error
    
    No route matches [GET] "/movies"

    Error messages like this are your friend. They're actually trying to help you fix (and understand) problems. Unfortunately, we're accustomed to errors being bad things, and when we see them we panic! Here's a little secret: Even the best Rails programmers get error messages—often! But they don't panic. Instead, good programmers actually read the error messages. Seriously, if you take the time to understand what Rails is trying to tell you in an error message, after a while you'll be able to quickly recognize (and fix) common errors.

    In this case, here's what happened: The web browser issued a request for the URL http://localhost:3000/movies. The http://localhost:3000 part of that URL is the address of the web server that fired up when we started the Rails app. So the web server received the request, then forwarded the /movies part of the URL on to the router. (Here comes the gist of the error message.) The router picked up the request and tried to find a route that matched a GET request for /movies. But we haven't told the router what to do with those types of requests, so we get the error.

  4. You tell the router how to handle certain requests by changing the config/routes.rb file. Open that file and remove all the comments until your file simply looks like this:

    Rails.application.routes.draw do
    end
    

    What we're left with is an empty Ruby block. Out of the gate, the router doesn't have any routes.

  5. Returning to our objective, when the router receives a GET request for /movies, we want a listing of movie titles to get sent back to the user's browser. The router's role in that process is simply to call some Ruby code we write to handle the request. In Rails parlance, the router calls an action (a Ruby method) that's defined in a controller (a Ruby class). The syntax for adding a route is a bit unorthodox, so here's a reminder of the generic format:

    verb "url" => "name_of_controller#name_of_action"
    

    The stuff on the left-hand side of the => identifies what the request looks like and the stuff on the right-hand side identifies the code to run to handle that request. We know what the request looks like, but what should we use as the names on the right-hand side?

    Because we want a listing of movies, by convention the name of the controller will be movies and the name of the action will be index. We'll actually create those a bit later, but for now it's enough just to know their names.

    In the config/routes.rb file, add a route that maps a GET request for /movies to the index action of the movies controller.

    Show Answer

    Hide Answer

    Rails.application.routes.draw do
      get "movies" => "movies#index"
    end
    
  6. Refresh your browser (you're still accessing http://localhost:3000/movies) and this time you should get a different error message:

    Routing Error
    
    uninitialized constant MoviesController

    Oh no, another error message! Yup, and you're not panicking. You're calmly reading what it says. It tells us that the route we added is trying to do what we told it to do. It's trying to send the request to a controller. We said the name of the controller was movies, but the router applied a naming convention and went looking for a Ruby class named MoviesController. We don't have a class with that name in our project, so the next step is to create it.

  7. Back at your command line prompt, inside of the flix project directory, generate a MoviesController class by typing:

    rails generate controller movies

    Or if you're feeling frisky, you can use the shortcut:

    rails g controller movies

    Note that the generator takes the name of the controller, which by convention should be plural. In this case, we want a controller named "movies".

  8. You should end up with an empty MoviesController class defined in the app/controllers/movies_controller.rb file, like so

    class MoviesController < ApplicationController
    end
    

    Although it's not important right now, it's worth noticing that the class inherits (subclasses) from the ApplicationController class. ApplicationController is the parent class of all controllers and can be found in the app/controllers/application_controller.rb file. It's through this inheritance relationship that MoviesController knows how to act like a controller.

  9. Refresh your browser again and this time you should get yet another (different!) error message:

    Unknown action
    
    The action 'index' could not be found for MoviesController

    OK, now we're getting somewhere. This time the router found the MoviesController class, and then tried to call the index action. That's what the route says to do. But as the error so smartly points out, the MoviesController doesn't have a method named index.

  10. In the MoviesController class, define an empty index action. Remember, an action is simply a publicly-accessible Ruby method defined in a controller class.

    Show Answer

    Hide Answer

    class MoviesController < ApplicationController
      def index
      end
    end
    
  11. Refresh your browser again and—you guessed it!—you should get another error message (it's like we're poking a hole through our app, one error message at a time):

    No template for interactive request
    
    MoviesController#index is missing a template...

    We're almost there! Again, the error message is fairly helpful. The router has now successfully sent the request to the index action of the MoviesController. The index action runs, but we're missing something. We need to send a list of movie titles back to the browser. To do that, the action needs to render a view template which in turn generates HTML. So we get the error because we don't have a view template for the index action.

    Wait, our index action is empty. So how does Rails know which view template to render? The short answer is "convention over configuration." Whenever an action runs and you don't explicitly tell it which view template to render, Rails uses a simple naming convention to find the corresponding view template. In this case, because the name of the action is index, which is defined in the MoviesController class, Rails assumes it should look for a view template file called index.html.erb in the app/views/movies directory. And the error message tells us that we're missing that file.

  12. Create a file named index.html.erb in the app/views/movies directory.

    Then inside the new file, add the following HTML snippet:

    <ul>
      <li>Iron Man</li>
      <li>Superman</li>
      <li>Spider-Man</li>
    </ul>
    
  13. Make sure to save your new file!

  14. Refresh your browser, and this time you should be rewarded with a list of blockbuster movie titles.

Excellent! Now we know that the request is successfully flowing through our application: from the router, to the action in the controller, through the view template, and back out to the web browser.

2. Use an Instance Variable

This works, but we'd like to make things more dynamic. After all, these are superhero movies, and they deserve a little POW! The first step toward that is to realign the responsibilities of the view and the controller.

Currently the view has a hard-coded list of movie titles. We'll likely want to add or remove movies, and later on we'll want to pull them from a database. To that end, we'd rather the view not know the specifics of how movies are created. The view should only be concerned with how to display the movies. It's the controller action's job to set up data for the view to display. And the way an action passes data to a view is by setting instance variables. So any data we want to make available to the view template needs to be assigned to instance variables in the action.

  1. In the index action, assign an array of movie titles (strings) to an instance variable named @movies.

    Show Answer

    Hide Answer

    def index
      @movies = ["Iron Man", "Superman", "Spider-Man"]
    end
    
  2. As a quick check that the movies are accessible in the view, add the following ERb snippet to the bottom of the index.html.erb view template:

    <%= @movies %>
    
  3. Refresh your browser and you should see the movie titles displayed in an array format, like this:

    ["Iron Man", "Superman", "Spider-Man"]

That format isn't very user friendly (unless all your users are programmers), but we'll fix that next...

3. Generate the List Dynamically

Finally, we want to replace the hard-coded list of movie titles in the index.html.erb file with a dynamically generated list that reflects the titles in the @movies instance variable. To do that, we'll need to use a mix of HTML and Ruby code in the index.html.erb file. We'll use Ruby to iterate through all the strings in the @movies instance variable. And for each movie title, we'll generate an HTML list item.

This is where the name of view template files becomes important. The name of our view file is index.html.erb. It has three parts: index is the name of the action, html is the type of content the view generates, and erb is the templating system used to generate the content.

In a Rails app, ERb (Embedded Ruby) is the default templating system. It lets us mix Ruby code with HTML in a view template. Then, when the view template is rendered, the embedded Ruby code runs and we end up with a dynamically generated response.

So a typical Rails view template is just a mixture of HTML tags and ERb tags. ERb tags come in two flavors:

  • <%= a Ruby expression %>: runs the Ruby expression and substitutes the result of the expression into the view template
  • <% a Ruby expression %>: runs the Ruby expression but does not substitute the result into the template (no text is generated). This form is typically used for Ruby conditonal and control flow statements.

The difference between the two is really subtle: the first flavor has an equal sign (=). Every Rails programmer on the planet has at one point mistakenly forgotten to include that extra character. Then they scratched their head for a few minutes wondering why nothing showed up in their browser. Consider yourself warned!

  1. In the index.html.erb file, use Ruby to iterate (loop) through each movie title in the @movies array. For each movie title, generate an HTML li element that contains the movie title. The goal is to end up with the same HTML as the hard-coded list of movie titles.

    Show Answer

    Hide Answer

    <ul>
    <% @movies.each do |movie| %>
      <li><%= movie %></li>
    <% end %>
    </ul>
    
  2. Once you have a nicely-formatted listing of all the movie titles that were set up in the index action, go ahead and remove the hard-coded list of movie titles. Follow that up with a celebratory KA-CHOW!

  3. Finally, now that you've separated the concerns of where the data comes from (the controller action) from how it's displayed (the view template), you can easily make changes in one place without adversely affecting the other. For example, add another movie title to the @movies array.

    Show Answer

    Hide Answer

    class MoviesController < ApplicationController
      def index
        @movies = ["Iron Man", "Superman", "Spider-Man", "Batman"]
      end
    end
    

    Refresh and the new movie title should be displayed in the listing. What's important here is you didn't have to change the view template. It simply takes whatever data it's given and displays it.

The benefits of this separation of concerns aren't very pronounced at this point, but being able to make changes in one place becomes increasingly important as the application grows.

Solution

The full solution for this exercise is in the views-and-controllers directory of the code bundle.

Bonus Round

View the Log File

It's useful to have a look at what happens behind the scenes. Every time Rails handles a request it leaves an audit trail in the log file.

Bring the window where your Rails server is running front and center, and then refresh the movie listing page in your browser. You should see something like this in the server window:

Started GET "/movies" for ::1 at 2019-05-01 17:45:27 -0600
Processing by MoviesController#index as HTML
  Rendering movies/index.html.erb within layouts/application
  Rendered movies/index.html.erb within layouts/application (Duration: 0.1ms | Allocations: 8)
Completed 200 OK in 8ms (Views: 7.6ms | ActiveRecord: 0.0ms | Allocations: 6592)

Notice that it shows the incoming request, the controller and action that handled it, and the view template that was rendered. Every time you refresh the page, this request-response cycle happens. Go ahead and refresh a few more times just to give Rails a workout.

Now, when you close the window you'll lose that information, but fear not. A permanent record of every request is kept in the log/development.log file. In fact, what you see in the console window is simply a reflection of what Rails adds to the log file.

So, when you want to know what really happened, peek at the log file.

Wrap Up

Whew! We accomplished a lot in this exercise. We took a stock Rails app and customized it to display a listing of movies. Not too shabby for the first exercise. As with learning anything new, the initial hurdle is to get comfortable with the vocabulary. We touched on all the major stages a request goes through when it enters a Rails app. Along the way you:

  • added your first route
  • generated your first controller
  • defined your first action
  • wrote your first view template

Early on we said that Rails uses an "MVC" design. In this section we focused on the view (V) and controller (C) to get something up and running quickly. In the next section we'll shift gears and focus on the model (M) which will let us put the movies in a database.

Onward and upward to the next section!

Dive Deeper

If you're new to the Ruby programming language or found yourself struggling with the Ruby syntax in this exercise, check out our Ruby course. It follows the same style as this course with both videos and exercises, and by the end of it you'll really understand Ruby syntax, design principles, and techniques. And once you're comfortable with the Ruby language, you'll have a much smoother ride building Rails apps.

Here's what some of our alumni have said about the Ruby course:

"The course is outstanding! The best I've ever taken. Rails is so much less mysterious now that I'm learning Ruby the right way."

— Tony Barone

"I wish I had done this course before starting Ruby on Rails development. Loved it!"

— Dr. Ed Wallitt

"I've been developing Ruby on Rails websites and apps for two years. This course gave me dozens and dozens of 'Ah-ha!' moments with regards to what is actually going on in my Rails apps. My confidence for building new Rails apps and refactoring my existing apps has increased tremendously."

— Chip Ashby