As a web developer, my first framework ever was RubyOnRails and I still keep a particular affection among them.
So when the template rendering was first introduced to me, I understood how it worked on the top layers, used it and I was perfectly fine doing so, because rails’ Convention over Configuration is very powerful.
But the part of me whom re coded several sys calls to understand how it works is craving to know what’s under the templates' system in RoR, so let’s dive in!
But first of all, a warning. I’m not here to talk to you about how to use template rendering in rails, many great articles have been written on the subject already and the documentation is super explicit so if that is the reason you are here, I’d suggest you have a look here or here.
How to trigger template rendering process ?
Actually there are many fun ways to trigger template rendering in Rails, but we will stick to controller here.
1 – Convention over configuration
The very first thing you ever try, when you first launch a server on a rails new my_project_name
brand-new project, is in the welcome controller provided by rails. Please note the layout: false
, we’ll get back to that later.
class Rails::WelcomeController < Rails::ApplicationController # :nodoc:
layout: false
def index
end
end
Processing by Rails::WelcomeController#index as HTML
Rendering /Users/valeriane/.rvm/gems/ruby-2.5.1/gems/railties-5.1.6.1/lib/rails/templates/rails/welcome/index.html.erb
As we can see the index method is empty and the rendering of /rails/templates/rails/welcome/index.html.erb
is processed automatically.
2 – Explicit call to render method
def update
@book= Book.find(params[:id])
if @book.update(book_params)
redirect_to(@book)
else
render "edit"
end
end
In this update
method, we have an explicit call to the render method which is actually an ActionView helper.
If object update fails with given params, we want the user to be able to change theses params right away. So we ask Rails to render the edit
view instead of the update
view it would have rendered otherwise as we are in an update
method.
You can render many formats (JavaScript, JSON, plain text…) and add a ton of options, so be sure to check the full documentation.
3 – Rendering HTML headers only
def show
if !params[:id]
head :bad_request
else
@book = Book.find(params[:id])
end
end
head method is used to send specific headers only responses to the browser. Here the :bad_request
symbol represents the 400 HTTP code. This usage is not suitable for production.
As this is not using the template rendering process we won't discuss head
further here.
4 – Hey, there was a redirect_to in example 2!
You are absolutely right.redirect_to
is a method that sets the response by default to 302 HTTP code and adds default instructions to tell browsers which request to build next.
As for render there is a large list of options you can provide, read the docs!
Once this instruction is sent to the browser, nothing will happen until the server receives the new request built by the browser. At this point, the process of handling request will restart from the beginning and even if there is a new redirect_to
in your way you will fatally end up encountering a head method, an implicit or an explicit render method at some point… or a 310 status code if you don't!
Just keep in mind that redirect_to
is setting the response, not triggering it, the code written after a redirect_to
will still be executed until the function returns.
Render uh?
As seen previously, if we want to serve our own html.erb files, we have to use render
explicitly or not.
Let's use this opportunity to clear up some Rails' black magic and use this very simple controller with implicit render method
class ExercicesController < ApplicationController
def index
end
end
At the very beginning it comes from the Rendering helper required in ActionController::Base. This way whenActionController::Base#render
is called, it is actually the method located in ActionView::Helpers::RenderingHelper
. Please have a look at the source code.
From this point, there are a lot of things going on, so let's check on the steps!
The steps
ActionView::Helpers::RenderingHelper
From this render method code will decide if it must continue processing for a partial or for a template. We will here focus on templates.
def render(options = {}, locals = {}, &block)
case options
when Hash
if block_given?
view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block)
else
view_renderer.render(self, options)
end
else
view_renderer.render_partial(self, partial: options, locals: locals, &block)
end
end
But no matter if it is a template or a partial, there is always a call to a view_renderer objects method. This is step 2.
ActionView::Renderer
Here is the render method of the Renderer
class. From the precedent function, we arrive directly to the render_template
method line 43, but we can also pass through the render
method wich will just determine wich method should be used, render_template
or render_partial
.
Also you can see in TemplateRenderer.new(@lookup_context).render(context, options)
the use of the @lookup_context
instance variable. Thats our next point.
ActionView::TemplateRenderer
@lookup_context
as commented in the source code "is the object responsible for holding all information required for looking up templates, i.e. view paths and details", and is a very complete object so take a deep breath and let's dive step by step into it by following the progression in the render
method of the TemplateRenderer.
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
@view = context
@details = extract_details(options)
template = determine_template(options)
prepend_formats(template.formats)
@lookup_context.rendered_format ||= (template.formats.first || formats.first)
render_template(template, options[:layout], options[:locals])
end
[...]
end
First of all, the TemplateRenderer as well as the PartialRenderer inherits from the AbstractRenderer and can access its methods.
In TemplateRenderer#render
, @view is the context variable given in the args when we called the render method previously. @details uses extract_details
method from ActionView::AbstractRenderer
accessible by inheritance. Template is obtained by passing options to determine_template
, a private method in this controller.
Then prepend_formats
, another method from AbstractRenderer is given attributes template.formats
. Then the format to render is set if not already on the @lookup_context
object. And finally render_template
is called
extract_details
It's the first real encounter with @lookup_context
. We can see in the source code of the class that registred_details is a module accessor.
def extract_details(options) # :doc:
@lookup_context.registered_details.each_with_object({}) do |key, details|
value = options[key]
details[key] = Array(value) if value
end
end
The extract_details
method iterates on @lookup_context.registered_details
and creates a hash of arrays filled with matching keys between options and the registred_details keys.
Wanna put your hands on? Just open a $ rails console
and type in $ ActionView::LookupContext.registered_details
to see the default values. You can also play around with $ ActionView::LookupContext.fallbacks
.
determine_template
determine_template is a private method checking for different keys in the options param. It looks for what kind of template it must find, and in our case it will end up passing through this condition
elsif options.key?(:template)
if options[:template].respond_to?(:render)
options[:template]
else
@lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
end
At this point we dont have any template key in our options hash, we will pass in the else condition and use LookupContext#find_template which is actually an alias of LookupContext#find.
find
just delegates to @view_path.find
where @view_path.find
== PathSet#Find.
There are more delegation games in this file, but finally we arrive at private method PathSet#_Find_All
find_all
is where Rails looks for the files using Resolver#find_all in a loop.
def _find_all(path, prefixes, args, outside_app)
prefixes = [prefixes] if String === prefixes
prefixes.each do |prefix|
paths.each do |resolver|
[...]
templates = resolver.find_all(path, prefix, *args)
[...]
return templates unless templates.empty
[...]
Resolver#find_all
actually calls PathResolver#find_templates
, where PathResolver#query
is called and a new Path instance is built with path = Path.build(name, prefix, partial).
But you already understood a lot today (you can be poud of yourself already!) on how the templating works in RubyOnRails so take a break and come back next time for the next parts.