Disassembling Rails — Template Rendering (2)

This is the second part of Disassembling Rails — Template Rendering (it’s about half a year ago, sorry!). In this post, I’m going to explain how does Rails renders your templates from Ruby objects.

File to checkout

- actionview/lib/action_view/renderer/template_renderer.rb
- actionview/lib/action_view/template.rb

Recall the previous step

At the end of the last post, I showed you that Rails will read your template files and initialize template objects using them.

# actionview/lib/action_view/template/resolver.rb

def query(path, details, formats, outside_app_allowed)
query = build_query(path, details)

template_paths = find_template_paths(query)
......
template_paths.map do |template|
......
contents = File.binread(template)

Template.new(contents, File.expand_path(template), handler,
virtual_path: path.virtual,
format: format,
variant: variant,
updated_at: mtime(template)
)
end
end

How does Rails use the template object?

After ActionView::TemplateRenderer found the template, it’ll call #render_template and pass the template object as an argument. Also notice that it passes the layout and locals as well, and they are extracted from the options.

# actionview/lib/action_view/renderer/template_renderer.rb

module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
……
# Found your template
template = determine_template(options)
……
render_template(template, options[:layout], options[:locals])
end
end
end

And then this is #render_template

def render_template(template, layout_name = nil, locals = nil)
.....

render_with_layout(layout_name, locals) do |layout|
# instrumenting block
template.render(view, locals) { |*name| view._layout_for(*name) }
# end
end
end

You can see that the actual rendering is wrapped with render_with_layout block. I'm going to skip this part today though, there will be another post to explain the layout rendering.

Template#render

Template#render does its job in two steps: compile the template's content into a method (yes, a ruby method), and call that method. Simple enough, right?

def render(view, locals, buffer = nil, &block)
......
compile!(view)
view.send(method_name, locals, buffer, &block)
end

What does compile mean?

It means that Rails will define a method on ActionView::Base (more correctly, ActionView::CompiledTemplates) and that method will take locals and return a string. Let me explain it with examples: assume we want to render a say_hi template that takes one local:

# say_hi.erb
Hi <%= name %>

And after it’s compiled to a method, it’ll be something like this to Rails:

def say_hi(local_assigns)
name = local_assigns[:name]
“Hi #{name}”
end

view.say_hi(name: "Stan")
#=> Hi Stan

So how does it do that? Well, Rails takes full advantage to Ruby’s meta-programming support. Especially the module_eval method. This is how it's done:

require "erb"
# let's pretend it's ActionView::Base
class View
end

class Template
def initialize(name, content)
@name = name
@content = content
end

def render(view)
compile(view)
view.send(@name)
end

def compile(view)
# we only need to compile it once
return if @compiled
# use erb as template engine
body = ERB.new(@content).src
src = <<-end_src
def #{@name}
#{body}
end
end_src

view.singleton_class.module_eval(src)

@compiled = true
end
end

view = View.new
template = Template.new("say_hi", "Hi!")
template.render(view) #=> Hi!
view.methods.first #=> :say_hi

But how about the locals? How can we pass locals dynamically if Rails only defines one method once for every template? To me, that’s the most brilliant part of this design. Let us update the code a little bit:

require "erb"

class View
end

class Template
def initialize(name, content, locals)
@name = name
@content = content
@locals = locals
end

def render(view, local_assigns)
compile(view)
view.send(@name, local_assigns)
end

def compile(view)
return if @compiled
body = ERB.new(@content).src

src = <<-end_src
def #{@name}(local_assigns)
#{locals_code}
#{body}
end
end_src

view.singleton_class.module_eval(src)

@compiled = true
end

def locals_code
@locals.map do |local|
"#{local} = local_assigns[:#{local}]"
end.join("\n")
end
end

view = View.new
template = Template.new("say_hi", "Hi! <%= name %>", [:name])
template.render(view, name: "Stan") #=> Hi! Stan

In our example, the full definition of say_hi method will be:
 ``
 def say_hi(local_assigns)
 # generated by
locals_code`
 name = local_assigns[:name]

# belows are generated by ERB, and expanded for readibility
 _erbout = +’’
 _erbout.<< “Hi! “.freeze
 erbout.<<(( name ).tos)
 _erbout
 end
 ```

Isn’t it clever? I was impressed when I first read this implementation. And it’s been around for a decade!

Of course, the actual #compile method is far more complicated for handling different cases, we can check it here if you're interested.

BTW, you can check this mechanism in Rails console by yourself.

# remember to rename the `posts` snd `posts/index` to what you really have in your app
paths = ActionController::Base.view_paths
view = ActionView::Base.new(paths)

# we don't have the compiled method yet
view.methods.grep(/posts/) #=> []

# this render is from action_view/helpers/rendering_helper.rb, remember?
view.render(template: "posts/index")

# we have it now!
view.methods.grep(/posts/) #=> [:_app_views_posts_index_html_erb___4416254959938662165_70267190542480]

Summary

To me, ActionView is a beautifully designed library. It finds and renders templates elegantly. And the fundamental design is barely changed in the past 10 years and doesn't really have any serious issue! I've learned a lot of stuff from reading its codebase, how you can find some time to dig into it as well, it's a great investment :-)