You'll encounter this in new Rails apps inside your application.html.erb
:
<body>
<main>
<%= yield %>
</main>
</body>
But how does yield
work inside of the layout template? Where does it get executed and how does it know to yield the right view in between the layout?
First of all, you'd need to understand Ruby's yield
syntax itself. There's a lot of good material about it in the Internet, but in short:
You define a method that takes a block:
def got_it(&block)
puts "before yield"
yield
puts "after yield"
end
And when executing that method, you give it a block as you do with other common methods like .each { puts "block inside of curly braces" }
or .map do "also a block" end
.
So in our case we do:
> got_it { puts "in between yield" }
Being equipped with that knowledge, it might mean that somehow application.html.erb
gets called with a block that contains the to be displayed view contents which then get yielded between the layout template's <main>
tags.
So let's go a step back to the beginning and see how this actually plays out in Rails app and where things are happening.
- Client issues HTTP request
- A route gets hit
- A Controller action gets hit
- There is an implicit or explicit
render
call
E.g. here:
# GamesController
def index
@games = Game.all
# Here render gets called implicitly if you donβt write it yourself.
end
Now render
is quite a beast. I've dug the relevant stuff in the Ruby Guides Layouts and Rendering Section and had a few pointers, so I tried looking into where the flow of render
leads in the AbstractController::Rendering
module. That's a module included in every controller that inherits from ActionController::Base
. So basically most controllers that you'll see in the wild.
The Ruby Guides also mention that the logic for rendering stuff is in ActionView::Template::Handlers
.
There's tooooons of stuff happening with a lot of moving parts. So let's try to simplify this a bit.
Ruby distributions have ERB included by default:
$ irb
> require 'erb'
=> true
> ERB.new("2 + 2 is <%= 2 + 2 %>").result
=> "2 + 2 is 4"
> ERB.new("YOUR views.html.erb files get transformed to strings here")
That's it, all your template rendering is based on this kind of code π Somewhere deep down in the render method hierarchy, ERB takes some input from you (your ".html.erb"
files) and turns it into a String which is sent back to the user as a document response via HTTP.
But Rails is cooler than ERB, they don't use ERB directly anymore but Erubi, a different gem that is said to be faster and more robust. Basically, Erubi is a slightly different and optimized implementation of 'erb'.
The above example in Erubi is a bit different, a bit more eval:
$ gem install eruby
$ irb
> require 'erubi'
> eval(Erubi::Engine.new("2 + 2 is <%= 2 + 2 %>").src)
Let's tie all of this together now. We said there is this process:
- Client issues HTTP request
- A route gets hit
- A Controller action gets hit
- There is an implicit or explicit
render
call
Now we know that render
brings things together. It takes the view name, renders the layout and passes the view as a block to the layout. Let's look at an simplified code example to illustrate it.
In some of our controllers, something like this happens:
def index
@games = Game.all
# render(:index) is called implicitly here.
end
This is how render
could look like in its simplest form:
def render(template)
render_with_layout do
render_template(template)
end
end
It runs a render_with_layout
method that takes a block where render_template
generates the template HTML string for the requested :index
view.
def render_with_layout
# This could come from some ERB file, like application.html.erb
default_layout = <<-END
<head> ... lots of heady stuff </head>
<body>
#{yield}
</body>
END
eval(Erubi::Engine.new(default_layout).src)
end
render_with_layout
takes a default layout in this case that could come from some application.html.erb
but here we just store it in local variable for simplicity.
What's getting "inserted" in-place of the yield
is the code from render_template
(because it's returned by the block of render_with_layout { render_template }
, right?):
def render_template(partial_name)
eval(Erubi::Engine.new(File.read("#{partial_name.to_s}.html.erb")).src)
end
So, you can imagine this:
default_layout = <<-END
<head> ... lots of heady stuff </head>
<body>
#{yield}
</body>
END
More like this:
default_layout = <<-END
<head> ... lots of heady stuff </head>
<body>
eval(Erubi::Engine.new(File.read("#{partial_name.to_s}.html.erb")).src)
</body>
END
And in the end, the default_layout
is rendered at the end of the render_with_template
:
eval(Erubi::Engine.new(default_layout).src)
Check it out in the repo if you'd like to play around with it yourself: here
So hopefully this article helps to dullify some magic and to get the gist of the initial question about how yield
works in a Rails' application.html.erb
.
Your Turn
If you've been to any of my workshops or talks, you'll now that I'm a big fan of learning techniques: Recall, Spaced Repetition, Deliberate Practice, and Mnemonics.
Here are some exercises to pump this article into your brain if you feel like it:
Deliberate practice
Write your own render
method where render_with_layout
takes a :custom_layout
parameter. You might notice that having the default_layout
inside of an application.html.erb
is tougher to implement. You can make use of methods though, if you'd like to keep things simple.
Recall
Now it gets a bit esoteric, but after completing this article, close your eyes and do a mental exercise. Imagine a request calling a show
action on your favorite resource. Go the way down from routes over the controller up to the point where the whole yielding stuff happens. Do you see the flow?
Spaced Repetition
Add a reminder to repeat the above recall exercise again in 3 days.
References
If you'd like to dig even deeper yourself, use some resources and examples that I used for this post:
- RailsGuides
- Rebuilding Rails by Noah Gibbs (chapter about ERB and templates)
- erubi: https://github.com/jeremyevans/erubi
- ideas for how to render on SoF
erubi examples with yield and blocks:
Comments or questions? Just tweet it out to me!
Tweet to @RichStoneIO