Really understand Rails templating

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. 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 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 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 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.

Blog

À lire également

clever cloud fest. is coming

Clever Cloud announces its clever cloud fest. an event to bring together customers, prospects, partners and more.
Company

Clever Cloud: Discover our clever side

Faced with increasingly demanding technical environments, Clever Cloud has established itself as a key player in supporting tech teams and IT decision-makers.
Company

Metabase on Clever Cloud: easily query and visualize your data

Your business generates data, which you need to analyse, understand and make available to your teams, both technical and non-technical. To meet this need, we worked with David Sferruzza to integrate Metabase for the cloud, which is available on our Marketplace and can be easily deployed on Clever Cloud.
Company