4.4. Routes, Controllers, and Views¶
We’ve now been introduced to how Rails implements the models in MVC, but when users interact with a
SaaS app via a browser, they’re interacting with views and invoking controller actions, either by
typing URIs into their browser (resulting in an HTTP GET
) or interacting with page elements that
generate GET
requests (links) or POST
requests (forms). In this section we take a tour through views
and controllers to understand the lifecycle of such a request when it hits a Rails app. We first explore
the controllers and views corresponding to REST actions that only read model data: Index and Read. In
Section 4.6 we consider controllers and views corresponding to actions the modify data: Create, Update, and Delete.
As we know from Section 3.2, our app will receive a request in the form of an HTTP route. The first step in a
Rails app is therefore to determine which code in the app should be invoked to handle that route. Rails provides
a flexible routing subsystem that maps routes to specific Ruby methods in each controller using the contents of
the file config/routes.rb
. You can define any routes you like there, but if your app is RESTful (centered around
CRUD requests against a set of resources) and you abide by convention over configuration, the single line resources
’movies’
(in our case) defines a complete set of RESTful routes for a model (resource) called Movies, as Figure 4.4 shows.
Although “RESTful route and action” column in the table should look familiar from Sec- tion 3.2, we raise four questions about it:
The four CRUD actions plus the Index action should only need five routes; why are there seven?
Most Web browsers can only generate HTTP
GET
andPOST
requests; how can a browser generate a route such as Update, which uses HTTPPUT
?Some routes such as
show
include a variable (parameter) as part of the route URI, and others such ascreate
must also provide the attribute values of the entity to be created as parameters. How are these parameters and their values made available to the controller action?Finally, what are the “route helper methods” referred to in the table and why are they needed?
The first question—why seven routes rather than five—is easy but subtle. We preview the answer here and will
return to it when we discuss HTML forms in Section 4.6. A RESTful request to create a movie would typically
include information about the movie itself—title, rating, and so on. But in a user-facing app, we need a way
to collect that information in- teractively from the user, usually by displaying a form the user can fill in.
Submitting the form would clearly correspond to the create
action, but what route describes displaying the form?
The Rails approach is to define a default RESTful route new
that displays whatever is necessary to allow collecting
information from the user in preparation for a create
request. A similar argument applies to update
, which requires
a way to show the user an editable version of the existing resource so the user can make changes; this latter
action is called edit
in rails, and typically displays a form pre-populated with the existing resource’s attribute values.
Turning to the second question, for historical reasons Web browsers only implement GET
(for following a link) and
POST
(for submitting forms). To compensate, Rails’ routing mechanism lets browsers use POST
for requests that normally
would require PUT
or DELETE
. Rails annotates the Web forms associated with such requests so that when the request is
submitted, Rails internally changes the HTTP method “seen” by the controller to PUT
or DELETE
as appropriate. The
result is that the Rails programmer can operate under the assumption that PUT
and DELETE
are actually supported,
even though browsers don’t implement them. As a result, the same set of routes can handle either requests coming
from a browser (that is, from a human being) or requests coming from another service in a SOA.
What about routes that include a parameter in the URI, such as show, or those that must also include parameters corresponding
to attribute values for a resource, such as create
? As we will see in the code examples in this section (and you will have an
opportunity to experiment with in the next CHIPS), the Rails routing subsystem prepares a hash called params[]
that is made
available to the controller. With the above routes.rb
file as part of an app, typing rake routes
at the command line
(within the root directory of your app) will list all the routes implied by that file, showing wildcard parameters with the
colon notation introduced in Section 3.5. For example, the route for show will appear as GET /movies/:id
, which tells us
that params[:id]
will hold the actual ID value parsed from the URI. Further, as we will see, Rails provides an easy way to
generate an HTML form in which the form fields are named in such a way that another value in params
, in this example
params[:movie]
, is itself a hash of key/value pairs corresponding to a Movie
object’s attributes and their desired values.
This mechanism sounds more confusing than it actually is, as the code examples below will show.
Finally, what are “route helpers”? By convention over configuration, the route URIs will match the resource name, but as we’ll
see later, you can override this behavior. You might, for example, decide later that you’d rather have your routes built
around film
rather than movie
. But then any view in your app that references the old-style movie route URIs—for example,
the page that serves the form allowing users to edit a movie’s info—would have to be changed to film
. This is the problem
that route helpers solve: they decouple what the
route does (create, read, and so on) from the actual route URI. As the table suggests, the Ruby method movies_path
will return
the correct URI for the route “list all movies,” even if the URI text itself is changed later (or for “create new movie,”
if POST
is used as the route’s verb). Similarly movie_path(23)
will always return the correct URI for “show movie ID 23”
(or update, edit, or destroy movie ID 23, depending on which HTTP verb is used). The route helpers also make explicit what
the route is supposed to do, improving readability.
What about the controller methods (called controller actions in Rails) that handle each RESTful operation? Once again,
convention over configuration comes to the rescue. By default, the routes created by resources ’movies’
will expect to find
a file controllers/movies_controller.rb
that defines a class MoviesController
(which descends from the Rails-provided
ApplicationController
, just as models descend from ActiveRecord::Base). That class will be expected to define instance methods
index, new, create, show (read), edit, update,
and destroy,
corresponding to the RESTful actions of Figure 4.4.
Each of these controller actions generally follows a similar pattern:
Collect the information accompanying the RESTful request: parameters, resource IDs in the URI, and so on
Determine what ActiveRecord operations are necessary to fulfill the request.For example, the Index action might just require retrieving a list of all movies from the Movies table; the Update action might require identifying a resource ID from the URI, parsing the contents of a form, and using the form data to update the movie with the given ID (primary key); and so on.
Set instance variables for any information that will need to be displayed in the view, such as information retrieved from the database.
Render a view that will be returned as the result of the overall request.
That leaves only the last bullet point: how does each controller action select a view, and how is the information generated in the controller action made available to that view?
You should no longer be surprised to hear that part of the answer lies once again in convention over configuration.
Controller actions do not return a value; instead, when a con- troller action finishes executing, by default Rails will
identify and render a view named app/views/
model-name /action.html.erb
, for example app/views/movies/show.html.erb
for
the show action in MoviesController
. The Rails module that choreographs how views are handled is ActionView::Base
. This
view consists of HTML interspersed with Erb (Embedded Ruby) tags that allow the results of evaluating Ruby code to be
interpolated into the HTML view. In particular, any instance variables set in the controller method become available in
the view.
Re-reading the previous sentence should give you pause. Why would instance variables of one class (MoviesController
)
be accessible to an object of a completely different class (ActionView::Base
), violating all OOP orthodoxy? The simple
reason is that the designers of Rails thought it would make coding easier. What actually happens is that Rails creates
an instance of ActionView::Base
to handle rendering the view, and then uses Ruby’s metaprogramming facilities to “copy”
all of the controller’s instance variables into that new object!
The controller code is in class
MoviesController
, defined inapp/controllers/movies_controller.rb
(note that the model’s class name is pluralized to form the controller file name.) Your app’s controllers all inherit from your app’s root controllerApplicationController
(inapp/controllers/application_controller.rb
), which contains controller behaviors common to multiple controllers (we will meet some in Chapter 5 and in turn inherits fromActionController::Base
.Each instance method of the controller is named using
snake_lower_case
according to the RESTful action it handles, plus the two “pseudo-actions”new
andedit
.The view template for each action is named the same as the controller method itself, so the view for Showing a movie would be in
app/views/movies/show.html.erb
. Strangely but conveniently, each view has access to all the instance variables set in the controller actions.
There’s one last thing to notice about these views: they aren’t legal HTML! In particular, they lack an HTML DOCTYPE,
<html>
element, and its main children <head>
and <body>
. In fact, we need to put those elements in views/application.html.erb
,
which “wraps” all views by default, as Figure 4.6 shows.
# This file is app/controllers/movies_controller.rb
class MoviesController < ApplicationController
def index
@movies = Movie.all
end
def show
id = params[:id] # retrieve movie ID from URI route
@movie = Movie.find(id) # look up movie by unique ID
end
end
<h1>All Movies</h1>
<%= link_to 'Add Movie', new_movie_path, :class => 'btn btn-primary' %>
<div id="movies">
<div class="row">
<div class="col-8">Movie Title</div>
<div class="col-2">Rating</div>
<div class="col-2">Release Date</div>
</div>
<%- @movies.each do |movie| %>
<div class="row">
<div class="col-8"> <%= link_to movie.title, movie_path(movie) %> </div>
<div class="col-2"> <%= movie.rating %></div>
<div class="col-2"> <%= movie.release_date.strftime('%F') %> </div>
</div>
<% end %>
</div>
<h1>Details about <%= @movie.title %></h1>
<div id="metadata">
<ul id="details">
<li> Rating: <%= @movie.rating %> </li>
<li> Released on: <%= @movie.release_date.strftime('%F') %> </li>
</ul>
</div>
<div id="description">
<h2>Description:</h2>
<p> <%= @movie.description %> </p>
</div>
<%= link_to 'Edit this movie', edit_movie_path(@movie), :class => 'btn' %>
<%= link_to 'Back to movie list', movies_path, :class => 'btn btn-primary' %>
<!DOCTYPE html>
<html>
<head>
<title> RottenPotatoes! </title>
<link rel="stylesheet" href="https://getbootstrap.com/docs/4.0/dist/css/bootstrap.min.css">
<%= javascript_include_tag :application %>
<%= csrf_meta_tags %>
</head>
<body>
<div class="container">
<%- if flash[:notice] %>
<div class="alert alert-info text-center"><%=flash[:notice]%></div>
<%- elsif flash[:alert] %>
<div class="alert alert-danger text-center"><%=flash[:notice]%></div>
<% end %>
<%= yield %>
</div>
</body>
</html>
Self-Check 4.4.1. The route helper for Show or Update take an argument, as in movie_path(@movie),
but the route helpers for
New and Create ( new_movie_path
and movies_path
) do not. Why the difference?
The argument to the Show and Update route helpers is either an existing
Movie
instance or the ID (primary key) of an existing instance. Show and Update operate on existing movies, so they take an argument to identify which movie to operate on. New and Create operate on not-yet-existing movies.
Self-Check 4.4.2. Why doesn’t the route helper movies_path for the Index action take an argument? (Hint: The reason is slightly different than the answer to the previous question!)
The Index action just shows a list of all the movies, so no argument is needed to distinguish which movie to operate on.