Archives

Rebuild Rails Part 4

Now, it is time to build real pages in our super duper Tracks framework. We will support ERB and to do that we need the erubis gem.

# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
  def index
    locals = { title: "/posts/index" }
    render template: "posts/index", locals: locals
  end
end

# app/views/posts/index.html.erb
hello from tracks <%= title %>

# lib/tracks/controller.rb
def render(options)
  template_name = options.fetch(:template)
  locals = options.fetch(:locals)

  filename = File.join "app/views", "#{template_name}.html.erb"
  template = File.read(filename)
  erb = Erubis::Eruby.new(template)
  erb.result locals
end

$ ruby spec/application_spec.rb
Run options: --seed 12593

# Running:

..

Finished in 0.020498s, 97.5705 runs/s, 195.1410 assertions/s.

2 runs, 4 assertions, 0 failures, 0 errors, 0 skips

Our tests show we are good but it’s not even close to Rails' “magic”. First, let’s use the controller/action convention, i.e. if no template is passed to the render method, we should use the app/views/posts/index.html.erb template. We also modify our Controller to save the env data passed by Rack because we need to check the path.

# lib/tracks/controller.rb
module Tracks
  class Controller
    attr_reader :env, :controller_name, :action_name

    def initialize(env)
      @env = env
      extract_env_info
    end

    def extract_env_info
      _, @controller_name, @action_name, after = path_info.split("/")
    end

    def path_info
      env["PATH_INFO"]
    end

    def extract_template_name
      "#{controller_name}/#{action_name}"
    end
  end
end

We then update our render method to check for the template if it is not passed.

# lib/tracks/controller.rb
     def render(options)
-      template_name = options.fetch(:template)
+      template_name = options.fetch(:template) { extract_template_name }
       locals = options.fetch(:locals)

       filename = File.join "app/views", "#{template_name}.html.erb"
@@ -29,6 +43,5 @@ module Tracks
       erb.result locals
     end

Our next modification involves using the @instance_variables to pass values from the controller to the view files. To do that, we just need to pass the current binding to the eruby instance and it should pickup the instance variables we have in the controller.

In Rails, it is a bit more involved. There is a concept of view context. Rails collects the instance variables from the controller, then duplicates the values into the view context. The Ruby methods #instance_variables, #instance_variable_get, #instance_variable_set allow Rails to accomplish that.

def render(options={})
  template_name = options.fetch(:template) { extract_template_name }
  filename = File.join "app/views", "#{template_name}.html.erb"
  template = File.read(filename)
  erb = Erubis::Eruby.new(template)
  erb.result(binding)
end

We also update our render method and controller because we do not need the locals parameter.

# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
  def index
    @title = "/posts/index"
    render
  end
end

# app/views/posts/index.html.erb
hello from tracks <%= @title %>

We still have the extra render call in our controller. To remove it, we keep track of the call to render and if there’s no rendered result yet, we call render.

diff --git i/app/controllers/posts_controller.rb w/app/controllers/posts_controller.rb
index f7883e3..d7aa012 100644
--- i/app/controllers/posts_controller.rb
+++ w/app/controllers/posts_controller.rb
@@ -1,6 +1,5 @@
 class PostsController < Tracks::Controller
   def index
     @title = "/posts/index"
-    render
   end
 end
diff --git i/lib/tracks/controller.rb w/lib/tracks/controller.rb
index 0119c1b..3d5d75a 100644
--- i/lib/tracks/controller.rb
+++ w/lib/tracks/controller.rb
@@ -30,7 +30,12 @@ module Tracks

       controller_class_name = controller.capitalize + "Controller"
       controller_class = Object.const_get(controller_class_name)
-      controller_class.new(env).send(action)
+      controller_context = controller_class.new(env)
+      controller_context.send(action)
+
+      if controller_context.rendered_string.nil?
+        controller_context.render
+      end
+
+      controller_context.rendered_string
     end

     def render(options={})
@@ -38,7 +43,7 @@ module Tracks
       filename = File.join "app/views", "#{template_name}.html.erb"
       template = File.read(filename)
       erb = Erubis::Eruby.new(template)
-      erb.result(binding)
+      @rendered_string = erb.result(binding)
     end

We also update our controller and tests to cover the case of using render explicitly.

# app/controllers/posts_controller.rb
class PostsController < Tracks::Controller
  def index
    @title = "/posts/index"
  end

  def show
    @title = "/posts/index"
    render template: "posts/index"
  end
end

# spec/application_spec.rb
require_relative "spec_helper"

describe CrazyApp::Application do
  include Rack::Test::Methods

  def app
    CrazyApp::Application
  end

  it "should respond with /" do
    get "/"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from index.html"
  end

  it "should respond with different path" do
    get "/posts/index"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from tracks /posts/index"
  end

  it "should respond with different template" do
    get "/posts/show"
    last_response.ok?.must_equal true
    last_response.body.strip.must_equal "hello from tracks /posts/index"
  end
end