Disassembling Rails — Template Rendering (2)

Stan Lo
Stan Lo
Dec 18, 2018 · 4 min read

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 found the template, it’ll call and pass the template object as an argument. Also notice that it passes the and 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

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 block. I'm going to skip this part today though, there will be another post to explain the layout rendering.

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 (more correctly, ) and that method will take locals and return a string. Let me explain it with examples: assume we want to render a 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 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 method will be:
``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 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 apppaths = 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, 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 :-)

Ruby Inside

Ruby articles and posts

Stan Lo

Written by

Stan Lo

Creator of Goby language(https://github.com/goby-lang/goby), also a Rails/Ruby developer, Rails contributor. Love open source, cats and boxing.

Ruby Inside

Ruby articles and posts

Stan Lo

Written by

Stan Lo

Creator of Goby language(https://github.com/goby-lang/goby), also a Rails/Ruby developer, Rails contributor. Love open source, cats and boxing.

Ruby Inside

Ruby articles and posts

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store