2.12 Embedded Ruby in the Controller with Instance Variables
Now that we are RCAV pros, we can go back to the embedded Ruby we used to make /rock truly dynamic. Let’s see another (perhaps better) way of writing our embedded Ruby code.
Open the user_rock.html.erb
file:
<!-- app/views/game_templates/user_rock.html.erb -->
<h2>We played rock!</h2>
<% comp_move = ["rock", "paper", "scissors"].sample %>
<h2>
<%= comp_move %>!
They played </h2>
<% if comp_move == "rock" %>
<h2>We tied!</h2>
<% elsif comp_move == "paper" %>
<h2>We lost!</h2>
<% elsif comp_move == "paper" %>
<h2>We won!</h2>
<% end %>
{: mark_lines=“5”}
The highlighted code <% comp_move = ["rock", "paper", "scissors"].sample %>
is okay for this trivial example, but in a real application there may be dozens of lines of code to prepare the information for the user. We may lookup data from a database, do some math on it, find API data, and more. We want somewhere other than the HTML template to put this code.
Go back to the game_templates/user_paper.html.erb
file associated with /paper, since we didn’t get far there. We would like our file to look like this:
<!-- app/views/game_templates/user_paper.html.erb -->
<h2>
We played paper!</h2>
<h2>
<%= comp_move %>!
They played </h2>
<h2>
<%= outcome %>!
We </h2>
{: mark_lines=“7-13”}
We wish we could just do this and avoid all the lines of embedded Ruby that are in the previous user_rock.html.erb
view template. These computations really don’t belong here. The view templates should be given some data and then their job should just be to format and present it beautifuly and usably to the user. In the backend the responsibility should be marshalling the correct data and sending it to the view template.
Well, return to our controller file and add the following:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
false)
layout(
# Add your actions below this line
# ================================
def homepage
:template => "game_templates/rules.html.erb" })
render({ end
def play_rock
# write your code here
# redirect_to("https://www.wikipedia.org")
# render({ :html => "<h1>Hellow, world!</h1>".html_safe })
:template => "game_templates/user_rock.html.erb" })
render({ end
def play_paper
"rock", "paper", "scissors"].sample
comp_move = [
if comp_move == "rock"
"won"
outcome = elsif comp_move == "paper"
"tied"
outcome = elsif comp_move == "scissors"
"lost"
outcome = end
:template => "game_templates/user_paper.html.erb" })
render({ end
end
{: mark_lines=“23-31”}
STOP. Read the added code. Do you understand it? Yes? Okay, keep reading.
We have removed all of the embedded Ruby tags (<% %>
and <%= %>
) from the code we had in the user_rock.html.erb
view template and added a new outcome
variable to hold a String
that depends on the control flow associated with the random computer move comp_move
.
Now, when a user visits the route /paper, the action play_paper
in the controller ApplicationController
will be triggered, and the code will be run before the template is rendered.
So let’s try to visit /paper again. Oops, we get this error:
undefined local variable or method `comp_move' for #<#Class...
This error is coming from our user_paper.html.erb
view template, when the browser gets to the first embedded Ruby tag:
<!-- app/views/game_templates/user_paper.html.erb -->
<h2>
We played paper!</h2>
<h2>
<%= comp_move %>!
They played </h2>
<h2>
<%= outcome %>!
We </h2>
{: mark_lines=“8”}
That local variable comp_move
is undefined!
A local variable only exists in the scope it was defined. If we create a local variable in a loop, it will only exist in that loop. If I want some variable available outside the loop, then I would need to create it outside the loop and modify it in the loop. We can’t just use the comp_move
local variable in our template if we created it in the play_paper
method (action). The variable is effectively “dead” after play_paper
executes.
Then how do we make the controller variables available in the view template? We can do this by modifying our controller:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
false)
layout(
# Add your actions below this line
# ================================
def homepage
:template => "game_templates/rules.html.erb" })
render({ end
def play_rock
# write your code here
# redirect_to("https://www.wikipedia.org")
# render({ :html => "<h1>Hellow, world!</h1>".html_safe })
:template => "game_templates/user_rock.html.erb" })
render({ end
def play_paper
@comp_move = ["rock", "paper", "scissors"].sample
if @comp_move == "rock"
@outcome = "won"
elsif @comp_move == "paper"
@outcome = "tied"
elsif @comp_move == "scissors"
@outcome = "lost"
end
:template => "game_templates/user_paper.html.erb" })
render({ end
end
{: mark_lines=“23 25-30”}
All we did was place an @
character before any variable that we want access to in our view template. This is a new kind of variable called an instance variable. This type of variable will survive as long as the instance of the object in which it’s created survives.
When someone visits /paper, Rails creates an instance of the ApplicationController
Class and then executes the play_paper
method (because get
in config/routes.rb
told it how to respond to this route!). As long as the ApplicationController
instance is “alive” (until the response is sent to the user by rendering HTML in their browser), the instance variables produced by play_paper
will exist!
All that is to say, by adding @
, we now have @comp_move
and @outcome
available for our template. We just need to make sure those instance variables are also properly referenced in user_paper.html.erb
:
<!-- app/views/game_templates/user_paper.html.erb -->
<h2>
We played paper!</h2>
<h2>
<%= @comp_move %>!
They played </h2>
<h2>
<%= @outcome %>!
We </h2>
{: mark_lines=“8 12”}
Again, we just use the leading @
on our variables to tie them to the instance variables in the controller. And if we visit the /paper URL, it works! And we have a vastly improved organization.
Most computation work like this should go in the controller as we have done it here. We will have some cases where more embedded Ruby goes in the template (e.g. rendering database records with .each
loops, if
statements to check if a user is allowed to see something, other conditional statements). In general, if it can happen in the controller, it should happen there.
If /paper appears to work when you test it manually, then it’s time for a rails grade
and a /git commit.
2.12.1 Completed Code
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
false)
layout(
# Add your actions below this line
# ================================
def homepage
:template => "game_templates/rules.html.erb" })
render({ end
def play_rock
# write your code here
# redirect_to("https://www.wikipedia.org")
# render({ :html => "<h1>Hellow, world!</h1>".html_safe })
:template => "game_templates/user_rock.html.erb" })
render({ end
def play_paper
@comp_move = ["rock", "paper", "scissors"].sample
if @comp_move == "rock"
@outcome = "won"
elsif @comp_move == "paper"
@outcome = "tied"
elsif @comp_move == "scissors"
@outcome = "lost"
end
:template => "game_templates/user_paper.html.erb" })
render({ end
end
<!-- app/views/game_templates/user_paper.html.erb -->
<h2>
We played paper!</h2>
<h2>
<%= @comp_move %>!
They played </h2>
<h2>
<%= @outcome %>!
We </h2>