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>
  They played <%= comp_move %>!
</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>
    They played <%= comp_move %>!
</h2>

<h2>
    We <%= outcome %>!
</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
  layout(false)

  # Add your actions below this line
  # ================================
  
  def homepage
    render({ :template => "game_templates/rules.html.erb" })
  end

  def play_rock
    # write your code here
    
    # redirect_to("https://www.wikipedia.org")
    
    # render({ :html => "<h1>Hellow, world!</h1>".html_safe })

    render({ :template => "game_templates/user_rock.html.erb" })
  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

    render({ :template => "game_templates/user_paper.html.erb" })
  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>
    They played <%= comp_move %>!
</h2>

<h2>
    We <%= outcome %>!
</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
  layout(false)

  # Add your actions below this line
  # ================================
  
  def homepage
    render({ :template => "game_templates/rules.html.erb" })
  end

  def play_rock
    # write your code here
    
    # redirect_to("https://www.wikipedia.org")
    
    # render({ :html => "<h1>Hellow, world!</h1>".html_safe })

    render({ :template => "game_templates/user_rock.html.erb" })
  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

    render({ :template => "game_templates/user_paper.html.erb" })
  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>
    They played <%= @comp_move %>!
</h2>

<h2>
    We <%= @outcome %>!
</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
  layout(false)

  # Add your actions below this line
  # ================================
  
  def homepage
    render({ :template => "game_templates/rules.html.erb" })
  end

  def play_rock
    # write your code here
    
    # redirect_to("https://www.wikipedia.org")
    
    # render({ :html => "<h1>Hellow, world!</h1>".html_safe })

    render({ :template => "game_templates/user_rock.html.erb" })
  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

    render({ :template => "game_templates/user_paper.html.erb" })
  end

end
<!-- app/views/game_templates/user_paper.html.erb -->

<h2>
    We played paper!
</h2>

<h2>
    They played <%= @comp_move %>!
</h2>

<h2>
    We <%= @outcome %>!
</h2>