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:
-
Add a route to handle requests for
/movies
-
Generate a
MoviesController
and define anindex
action 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
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.
-
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. -
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
. Thehttp://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 aGET
request for/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.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.
-
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 beindex
. 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 aGET
request for/movies
to theindex
action of themovies
controller. -
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 namedMoviesController
. 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
flix
project directory, generate aMoviesController
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".
-
You should end up with an empty
MoviesController
class defined in theapp/controllers/movies_controller.rb
file, like soclass 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 theapp/controllers/application_controller.rb
file. It's through this inheritance relationship thatMoviesController
knows 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
MoviesController
class, and then tried to call theindex
action. That's what the route says to do. But as the error so smartly points out, theMoviesController
doesn't have a method namedindex
. -
In the
MoviesController
class, define an emptyindex
action. 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
index
action of theMoviesController
. Theindex
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 theindex
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 isindex
, which is defined in theMoviesController
class, Rails assumes it should look for a view template file calledindex.html.erb
in theapp/views/movies
directory. And the error message tells us that we're missing that file. -
Create a file named
index.html.erb
in theapp/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>
-
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.
-
In the
index
action, assign an array of movie titles (strings) to an instance variable named@movies
. -
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 %>
-
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!
-
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 HTMLli
element 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
index
action, 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
@movies
array.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.
4. View the Server Log
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 server log.
To see this, look in the Terminal or command prompt window where your Rails server is running. Then refresh the movie listing page in your browser. Then, back in the server log, you should see something like this:
Started GET "/movies" for ::1 at YYYY-07-08 15:42:39 -0700 Processing by MoviesController#index as TURBO_STREAM Rendering layout layouts/application.html.erb Rendering movies/index.html.erb within layouts/application Rendered movies/index.html.erb within layouts/application (Duration: 0.5ms | Allocations: 224) Rendered layout layouts/application.html.erb (Duration: 24.2ms | Allocations: 14785) Completed 200 OK in 27ms (Views: 26.2ms | Allocations: 16025)
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.
So when you want to know what really happened, have a look at the server log. It's also a great tool for trouble-shooting. For example, if you click a link or button and nothing seems to happen, peek at the server log for any error messages or other clues.
A permanent record of every request is kept in the
log/development.log
file. In fact, what you see in the Terminal or command prompt window where your Rails server is running is simply a reflection of what Rails adds to the server log file.
Solution
The full solution for this exercise is in the
views-and-controllers
directory of the
code bundle.
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. 🙌
We think you're gonna love it!