Views and Controllers
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
To make that work, we have three primary tasks:
Add a route to handle requests for
MoviesControllerand define an
indexaction that prepares an array of movie titles
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.
Start by making sure you have the
flixapp running in a Terminal or command prompt window:
If the app is already running on port 3000, then you'll get an "Address already in use" error.
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
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:3000part 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
/moviespart 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
/movies. But we haven't told the router what to do with those types of requests, so we get the error.
You tell the router how to handle certain requests by changing the
config/routes.rbfile. 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.
Returning to our objective, when the router receives a
/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
moviesand 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.
config/routes.rbfile, add a route that maps a
indexaction of the
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.
Back at your command line prompt, inside of the
flixproject directory, generate a
MoviesControllerclass 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".
You should end up with an empty
MoviesControllerclass defined in the
app/controllers/movies_controller.rbfile, like so
class MoviesController < ApplicationController end
Although it's not important right now, it's worth noticing that the class inherits (subclasses) from the
ApplicationControlleris the parent class of all controllers and can be found in the
app/controllers/application_controller.rbfile. It's through this inheritance relationship that
MoviesControllerknows how to act like a controller.
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
MoviesControllerclass, and then tried to call the
indexaction. That's what the route says to do. But as the error so smartly points out, the
MoviesControllerdoesn't have a method named
MoviesControllerclass, define an empty
indexaction. Remember, an action is simply a publicly-accessible Ruby method defined in a controller class.
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
indexaction of the
indexaction 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
indexaction 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
MoviesControllerclass, Rails assumes it should look for a view template file called
app/views/moviesdirectory. And the error message tells us that we're missing that file.
Create a file named
Then inside the new file, add the following HTML snippet:
<ul> <li>Iron Man</li> <li>Superman</li> <li>Spider-Man</li> </ul>
Make sure to save your new file!
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.
indexaction, assign an array of movie titles (strings) to an instance variable named
As a quick check that the movies are accessible in the view, add the following ERb snippet to the bottom of the
<%= @movies %>
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
We'll use Ruby to iterate through all the strings in the
instance variable. And for each movie title, we'll generate an HTML list
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!
index.html.erbfile, use Ruby to iterate (loop) through each movie title in the
@moviesarray. For each movie title, generate an HTML
lielement that contains the movie title. The goal is to end up with the same HTML as the hard-coded list of movie titles.
Once you have a nicely-formatted listing of all the movie titles that were set up in the
indexaction, go ahead and remove the hard-coded list of movie titles. Follow that up with a celebratory KA-CHOW!
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
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.
The full solution for this exercise is in the
views-and-controllers directory of the
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.
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!
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.