NEW
Machine Learning on GPU is now avalaible with Clever Grid. Check it out   
Topics:

As a web developer, my first framework ever was RubyOnRails and I still keep a particular affection among it.

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 recoded 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 wrote 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 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. ruby on rails default home page

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 wich 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 will 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 specifc 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 furether 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 browser wich request to build next. As for render there are 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 begining 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 explicitely 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 begining it comes from the Rendering helper required in ActionController::Base. This way whenActionController::Base#render is called, it's actually the method located in ActionView::Helpers::RenderingHelper. Please have a look at the source code.

From this point, there is a lot of things going on, so let's see 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 object's 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 thru 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. That's 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 breathe 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 optionsand the registred_details keys.

Wanna put your hands on ? just open a $ rails consoleand 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 optionsparam. 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 don't have any template key in our optionshash, we will pass in the else condition and use LookupContext#find_template wich 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 buildt with path = Path.build(name, prefix, partial). Let's have a closer look to that part.

PathResolver#find_templates && #query, the magic explained

PathResolver class #find_templates and #query

def find_templates(name, prefix, partial, details, outside_app_allowed = false)
  path = Path.build(name, prefix, partial)
  query(path, details, details[:formats], outside_app_allowed)
end

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

  template_paths = find_template_paths(query)
  template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed

  template_paths.map do |template|
    handler, format, variant = extract_handler_and_format_and_variant(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
en

prepend_formats

It seems far aways but we are back in TemplateRenderer#render but not for long as inheritance leads us to AbstractRenderer#prepend_formats where the available formats are verified and set if needed on @lookup_context.formats

render_template

TemplateRenderer#render_template

ActionView::TemplateResolver

/////////////////////////

http://climber2002.github.io/blog/2015/04/06/digging-rails-how-rails-finds-your-templates-part-4/

https://medium.com/rubyinside/disassembling-rails-template-rendering-1-51795f579577

http://climber2002.github.io/blog/2015/02/21/how-rails-finds-your-templates-part-1/


Profile picture of Valeriane Venance
By Valeriane Venance

Developer Advocate @clever_cloud, Freelance web developer, backpack travelling and electronic music addict.