MVC Rails - Views

How to Implement Rails-like Views in Ruby

In this article, we will learn 'one' way to implement the controller-to-view data handoff using instance variables, just like Rails, and following the same conventions as Rails. Although it's a highly simplified implementation to keep things simple, I hope you'll find it fun and learn something new.

7 min read

When I first started learning Rails, one of the patterns that I thought was pure 'magic' was how you could access a controller's instance variables in the views.

# controllers/posts_controller.rb
class PostsController
  def show
    @post = Post.find(1)
  end
end

# views/posts/show.html.erb
<%= @post.title %>

For a long time, I didn't understand how this would work behind the scenes. I tried reading the source code a few times without success. I just wasn't familiar enough with Ruby's advanced metaprogramming techniques.

However, as part of the series on building a web application in Ruby without Rails, I tried to come up with a way to implement the above feature myself. And just last night, I got it working. So, in the fifth article in the series, we'll learn one way to implement the Rails views pattern, where a controller's instance variables are accessible in the view, in pure Ruby.

This post won't show how Rails actually implements the views behind the scenes. I'm only trying to mimic the external Rails API, i.e. making controller instance variables available in views.

I still plan to read the Rails source code to really understand how the views work under the hood (watch out for a blog post on that soon).

However, I still wanted to show what a simplest views implementation could look like, to de-mystify some of the magic and give an intuitive understanding of the data handover from controllers to views. Personally, I find these projects a lot of fun and I always learn a bunch of Ruby in the process. I hope you do, too.

If you haven't read the previous posts in the series, this one might not make any sense. You can read the earlier articles here: Let's Build a Webapp in Ruby without Rails.

Alright, enough rambling. Let's get started.

As things stand now, our controllers are returning a plain text response from the action method. The router calls the controller's action to get this plain text response and returns it to the browser.

require_relative 'application_controller'

class ArticlesController < ApplicationController
  def index
    'All Articles'
  end
end

To make it work like Rails, we'll want to assign this response to an instance variable:

require_relative 'application_controller'

class ArticlesController < ApplicationController
  def index
    @title = 'All Articles'
  end
end

Then, the instance variables @title will be used by the corresponding view, just like Rails.

<%# views/articles/index.html.erb %> 

<h1>
  <%= @title %>
</h1>

How should we go about this?

Solve a simpler problem first

Before trying to implement the above feature, let's try to solve a different, simpler problem. How can we access instance variables in a string template?

First, read this post that introduces the concept of binding in Ruby.

Understanding the Concept of Binding in Ruby
Binding is an elegant way to access the current scope (variables, methods, and self) in Ruby. Typically, you use it for building view templates and executing strings of Ruby code. The Ruby REPL also makes abundant use of binding. In this post, we’ll learn what binding is and how it works.

The basic idea behind binding is to store the current context in an object for later use. The Kernel#binding  method returns the current binding object. Think of this binding object as a wrapper that encapsulates the current programming environment, i.e. variables, methods, and even the self object.

However, the concept of binding also extends to the instance variables of a class. That means if you get the binding in the scope of an instance of a class, it contains the instance variables which you can use in an ERB template. The following example will make it clear.

require 'erb'

class Person
  def initialize
    @name = 'Akshay'
    @age = 30
  end

  def get_binding
    binding
  end
end

ak = Person.new

erb = ERB.new 'My name is <%= @name %> and I am <%= @age %> years old'
puts erb.result(ak.get_binding)

# Output
# ======
# My name is Akshay and I am 30 years old

In the above code, the Person#get_binding method returns the binding of an instance of the Person class. When we call ak.get_binding, it returns the binding of the instance ak and contains the instance variables @name and @age, which the ERB uses to fill the string template.

Now that you understand how a template can access the instance variables of a class, let's go back to the original problem. We'll implement it in three simple steps.

Step One: Refactor the Router

This is what we have in the Router's get method. To recap, given the path articles/index, we build an instance of a controller ArticlesController and dynamically call the index method on it to get the plain-text response.

def get(path, &blk)
  if blk
    @routes[path] = blk
  else
    if path.include? '/'  # 'articles/index'
      controller, action = path.split('/')  # 'articles', 'index'
      controller_klass_name = controller.capitalize + 'Controller'  # 'ArticlesController'
      controller_klass = Object.const_get(controller_klass_name)  # ArticlesController
      @routes[path.prepend('/')] = ->(env) {
        controller_klass.new(env).send(action.to_sym) # ArticlesController.new(env).index
      }
    end
  end
end

Before we make any changes, let's fix a mistake I made in the previous post.

💡
The String#prepend method modifies the original string, which we don't want. Hence, I'll replace it with '/' + path so path remains unchanged. This will also let us move the path-splitting and controller-building code inside the block, avoiding unnecessary work at the time of generating the routes.

Here's the revised get method. I also took the opportunity to rename a few variables and extract the controller-building method to keep it simple and concise.

def get(path, &blk)
  if blk
    @routes[path] = blk
  else
    if path.include? '/'  # 'articles/index'
      @routes['/' + path] = ->(env) {
        controller_name, action_name = path.split('/')  # 'articles', 'index'
        controller_klass = constantize(controller_name) # ArticlesController
        controller_klass.new(env).send(action_name.to_sym) # ArticlesController.new(env).index
      }
    end
  end
end

private

# input: 'articles'
# output: ArticlesController
def constantize(name)
  controller_klass_name = name.capitalize + 'Controller'  # "ArticlesController" (a string)
  Object.const_get(controller_klass_name)  # ArticlesController  (a class)
end

So far, we haven't changed any functionality yet.

Step Two: Add a render Method on the Controller

Let's create a new method called render on the ApplicationController class. We're adding it to the base controller class so that all controller classes will inherit it.

require 'erb'

class ApplicationController
  attr_reader :env
  
  def initialize(env)
    @env = env
  end

  # new method
  def render(view_template)
    erb_template = ERB.new File.read(view_template)
    erb_template.result(get_binding)
  end

  # new method
  def get_binding
    binding
  end
end

The new code does the same thing that we saw in our simplified example above. There're three important things to note here:

  1. It reads the string template from a file called view_template, which we'll learn more about later.
  2. Then it calls the get_binding method to get the binding in the context of the controller class, making all the instance variables available to the binding.
  3. Finally, it renders the response by calling the result method on the ERB template and passing the binding.

If you're not following me, don't worry. I promise everything will start making sense very soon!

Step Three: Render from Router

We're almost done. The reason we added the render method to the controller class is to call it from the router, once the instance variables are set.

Modify the router's get method so it looks like this.

def get(path, &blk)
  if blk
    @routes[path] = blk
  else
    if path.include? '/'  # 'articles/index'
      @routes['/' + path] = ->(env) {
        controller_name, action_name = path.split('/')  # 'articles', 'index'
        controller_klass = constantize(controller_name) # ArticlesController
        
        controller = controller_klass.new(env)          # controller = ArticlesController.new(env)
        controller.send(action_name.to_sym)             # controller.index
        controller.render("views/#{controller_name}/#{action_name}.html.erb")
      }
    end
  end
end

The most important point is that we've separated the action-calling and response-rendering mechanisms.

Before, we were simply calling the action method on the controller and returning whatever response the action method returned.

Now, we first call the action method so that it gets a chance to set the instance variables. Then, we call the render method we just created, and pass it the name of the dynamically generated view template views/articles/index.html.erb following Rails conventions.

Inside the render method, the instance variables are already set, since it's the same controller instance. Hence it can render the view template without any problem.

We now have all the pieces in place. The only thing remaining is to use them. Let's update the controller so it sets the instance variables and add a view template that uses the instance variables.

Wrap up: Update the Controller and Create the View Template

Modify the ArticlesController class, so that the index action only sets the instance variables, instead of returning the response.

class ArticlesController < ApplicationController
  def index
    @title = 'All Articles'
  end
end

Next, add the view template called index.html.erb inside a newly created views/articles directory.

<html>
  <head>
    <title>Application</title>
    <link rel="stylesheet" href="/public/style.css">
    <meta charset="utf-8">
  </head>

  <body>
    <main>
      <h1>
        <%= @title %>
      </h1>
    </main>
  </body>
</html> 

Don't worry, we'll extract all the layout code in a separate template in a future post.

That's it, we're done. Now restart the application and visit localhost:9292/articles/index in the browser. You should see the following page:

Dynamically Generated View
Dynamically Generated View

Any new instance variables you set in the controller's action method will be available to use in the view.

💡
Check out the final code in the GitHub repository in 'views' branch.

That's a wrap. In the upcoming posts, we'll:

  • Implement models just like Rails!
  • Improve the project structure and organization
  • Add unit tests
  • Handle errors and logging
  • Process form inputs along with query strings into a params object
  • Connect to the database to store and fetch data
  • Add middleware to handle specific tasks like authentication
  • and much more...

If those sound interesting to you, consider subscribing to the blog.

Trust me, it's going to be a lot of fun, so stay tuned!!


I hope you liked this article and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. If you're already a subscriber, thank you.