Building a Web Application Without Rails: Rendering Views Dynamically using ERB

This is the fourth article in the series on building awesome web applications with Ruby and Rails. We continue our journey to build a web application without using Rails to better understand how Rails works. Today, we'll learn how to render views dynamically with the ERB gem used by Rails.

Building a Web Application Without Rails: Rendering Views Dynamically using ERB
Ruby on Rails

In the last post, we set up a very basic project structure for our web application which returns a hard-coded string response directly from the Ruby code. In this post, we will separate the view from the application logic, and serve static files using the Rack::Static middleware. Finally, we'll use the ERB gem to generate dynamic views.

As things stand now, our application mixes the logic and the views together in a single file. This is not a good practice in software development, as it tightly couples them together. You can't change one of them without understanding or affecting the other.

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/html'
    }
    
    # This is the view
    response = ['<h1>Hello World!</h1>']
    
    [200, headers, response]
  end
end

We want to separate the application logic and the views. The benefit is that you can change the view without worrying about the logic, and vice versa.

You might have heard of the separation of concerns principle. This is what it means in the simplest form.

Let's fix this.

Set up Views

We will separate the view from the application logic by moving the response HTML out of the app.rb and to its own views directory, in a file named index.html .

<!-- views/index.html -->

<h1>Hello World!</h1>

Now update the app.rb to read the contents of this file to build the response. We will use the File#read method to read the file.

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/html'
    }
    
    response_html = File.read 'views/index.html'
    
    [200, headers, [response_html]]
  end
end

Refresh the browser to verify that everything is still working.

Make It Pretty!

Have you noticed that our application is very plain-looking? Let's add some style to it.

First, we will standardize the index.html by adding a proper HTML structure.

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

  <body>
    <main>
        <h1>Hello World!</h1>
    </main>
  </body>
</html> 

Notice that we are referring to a style.css file in the public directory.

Let's add a new public directory and add a  style.css file in it.

/* /public/style.css */

main {
  width: 600px;
  margin: 1em auto;
  font-family: sans-serif;
}

Now reload the page. But our page looks the same, and none of the styles are getting applied. If we check the response in the DevTools window, you can see it's loading the style.css file, but the contents are from the index.html file.

style.css

What's going on?

The reason is that we are still serving the contents of the index.html file when the request for style.css arrives, as we don't have any logic to differentiate the requests.

class App
  def call(env)
    headers = {
      'Content-Type' => 'text/html'
    }
    
    response_html = File.read 'views/index.html'
    
    # same response is sent no matter the request
    [200, headers, [response_html]]
  end
end

Hence it ignores the actual contents of the style.css file.

Let's fix it.

Serving Static Files

When a request for style.css arrives, we want to serve the contents of the style.css file. For that, we will use the Rack::Static middleware.

According to the documentation,

The Rack::Static middleware intercepts requests for static files (javascript files, images, stylesheets, etc) based on the url prefixes or route mappings passed in the options, and serves them using a Rack::Files object. This allows a Rack stack to serve both static and dynamic content.

Let's update the config.ru file to include the Rack::Static middleware.

require 'rack'
require_relative './app'

use Rack::Reloader, 0

# Serve all requests beginning with /public from the "public" folder 
use Rack::Static, urls: ['/public']

run App.new
Note: To learn more about Rack and the concept of middleware, check out my article: The Definitive Guide to Rack for Rails Developers

Restart the Puma server and reload the page. You can verify in the DevTools that our application sends the correct CSS this time, and our styles are getting applied.

Dynamically Rendering Views

As of now, our view is a static HTML. That means each time the page loads, our application renders the same HTML, no matter what.

We want a dynamic view, i.e., a view which can embed variables in it, to render a different page each time you reload the browser.

For this, we'll need a view template.

What's a view template? It's simply a file with predefined slots which will be filled dynamically.

Here's how we will convert the static view into a view template.

  1. Change the name of the index.html file to index.html.erb
  2. Update the <h1> tag to use a title variable instead of a hard-coded string.
<!-- views/index.html.erb -->

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

  <body>
    <main>
      <h1>
        <%= title %>  <!-- Note the % -->
      </h1>
    </main>
  </body>
</html> 

The above view template will insert the value of the title variable to generate the final HTML view that will be shown to the user.

Within an ERB template, Ruby code can be included using both <% %> and <%= %> tags. The <% %> tags are used to execute Ruby code that does not return anything, such as conditions, loops, or blocks, and the <%= %> tags are used when you want output.

The next question is, how to pass this title variable to the view from the app.rb file? For this, we will use the erb gem.

For learning more about the erb syntax, check out the Rails guides.

This gem allows you to add any Ruby code to a plain text document for dynamically generating a new document. I recommend reading its documentation.

Here's how it works in its essence.

require 'erb'

name = 'Akshay'
age = 30

template = ERB.new 'My name is <%= name %> and I am <%= age %> years old'
puts template.result(binding)

=> "My name is Akshay and I am 30 years old"
The Kernel#binding method returns a Binding object that wraps the current context, i.e. variables, methods, self along with the other information at any given time in the code for a later use.

The above code creates an instance of the ERB class with a template string. The result method on this object takes a Binding object, which we get by calling the binding method. Finally, it uses the variables defined in that binding object to render the final text.

Let's use the above structure to render the template from the app.rb file:

require 'erb'

class App
  def call(env)
    headers = { 'Content-Type' => 'text/html' }
    
    title = 'Ruby on Rails'
    template = ERB.new(template_html)
    response_html = template.result(binding)

    [200, headers, [response_html]]
  end

  def template_html
    File.read 'views/index.html.erb'
  end
end

Now reload the browser. If everything worked, you should see our (new) title Ruby on Rails on the page. Our application is generating views on the fly.

Making It Dynamic

Now you might have questions about the dynamic nature of the above code. We are still using the hard-coded string "Ruby with Rails". But the important part is that we are fetching the value from the title variable. Now that value can come from anywhere, which is the part that makes it dynamic.

To illustrate this, let's tweak our code to read the title from the query string instead of a hard-coded string.

We will add a new method named get_title which will parse the env hash to fetch the query string, and return its value.

require 'erb'

class App
  def call(env)
    headers = { 'Content-Type' => 'text/html' }
    
    title = get_title(env) # new code here
    template = ERB.new(template_html)
    response_html = template.result(binding)

    [200, headers, [response_html]]
  end

  def get_title(env)
    query = env['QUERY_STRING'] # "title=ruby"
    values = query.split('=')   # ["title", "ruby"]
    values[1]                   # ruby
  end

  def template_html
    File.read 'views/index.html.erb'
  end
end

Now append ?title=whatever to the existing URL and refresh the browser. You should see the browser display whatever text you typed in the URL.

Congratulations, you now have a fully dynamic web application. In the next article, I will introduce the concept of a router and controllers. Stay tuned!

I hope you found this article useful and that you learned something new.

If you have any questions or feedback, please send me an email. I look forward to hearing from you.

Please subscribe to my blog below if you'd like to receive future articles directly in your email. If you're already a subscriber, THANK YOU 🙏

Subscribe to Akshay's Blog

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe