Conditional Custom Templates with Action Pack Variants

tl;dr

Variants were introduced in Rails 4.1 making it possible to conditionally render a view template. Before Rails 4.1 we employed a custom technique dubbed MultipleTemplates to provide similar functionality. Here we present our MultipleTemplates approach and why we’re switching to Action Pack variants.

We’ve used conditional templates in our Rails API backend for quite some time now. Our main use for conditional templates is to reduce rendering times by including only the minimal amount of data. A careful profile of our response times initially led us to this approach.

Our website is comprised of two parts, a Rails app on the back-end that acts as an API for our data, and a separate Rails App on the front-end that converts the raw JSON into HTML pages to ship to the client. A while ago, our website was responding rather slowly because our design was very data heavy. We noticed that we could decrease our page load time by a factor of five if we eliminated all unnecessary data from our API JSON responses. This speedup came mostly from JSON rendering time on the backend, but a little of the savings came from view rending time on the front end as well.

If our website front-end were the only consumer of our API back-end then we would have just deleted all the unnecessary attributes from the Jbuilder view template and we would have been finished. However, our API back-end services a bunch of different apps so we can’t delete stuff willy-nilly. Instead, we only want to render a custom Jbuilder view template for requests originating from our web front-end.

Our coding philosophy is to hack stuff together as quickly as possible and then refactor if the code seems to solve an actual problem. We also like to keep our code base as small as possible so we can develop quickly. With this design philosophy in mind we decided to create a module that conditionally alters ActionView’s render method.

The search path for the render method usually looks for templates at views/controller_name/action_name.format. We decided it would be nice to be able to keep all of our custom templates together in a directory beneath the controller name like this views/controller_name/web/action_name.format. The idea then is to pass a parameter in the web request template: 'web' to indicate that we want to render the templates contained in web/ as opposed to the templates templates in the controller_name directory.

There were also two more requirements. We wanted our solution to have a very small footprint on our code so that it didn’t clutter up a bunch of controllers. And we wanted to be able to easily constrain MultipleTemplates to particular controller actions like so:

class AwesomeController < ApplicationController

  include MultipleTemplates
  around_action :multiple_templates, :only => [:index, :show]

  .
  .
  .

end

Thus, MultipleTemples is a module that can be included in any controller, and has only one method of the same name as an around_action

lib/multiple_templates

module MultipleTemplates
  def multiple_templates
    if params[:template]
      begin
        def render *args
          super self.controller_path + '/' + params[:template] + '/' + action_name
        end
        yield
      ensure
        def render *args
          super
        end
      end
    end
    yield
  end
end

multiple_templates is an around_action so it will execute some code before rendering the view, then render the view as the yield, then execute the code after the yield after the view renders. So, before the yield we redefine the render method to look for a template in a custom location if the template parameter is included with the request. The first argument of render takes the path to find the template, so we can tell it where to find the view template by just passing it the path string. In this case, we are hard coding it to be the one we want instead of the default Rails would normally use. Finally, after the view is rendered with our custom template, we reset the render method to its original definition so that the path argument will be available in subsequent calls (calls coming from the other consumers of the API).

In our case, requests going to our API backend only come from our other applications so they are secure. If instead, our API was public, we would definitely need to sanitize how the view path could be altered.

With the introduction of Action Pack variants, there is a cleaner way to conditionally render custom templates. To render out a custom template all that needs to be done is to set request.variant in the controller using a before action. For example, if we wanted to have a custom web template, we could do:

class AwesomeController < ApplicationController
  before_action do
    request.variant = :web if params[:template] == 'web'
  end
  .
  .
  .
end

Then, Rails would look for a template at views/controller_name/action_name.format+web. So, in the case of a request to the index action of a controller called AwesomeController Rails would look for a template at views/awesome_controller/index.json+web.jbuilder. This is pretty cool because we don’t have to mess around with render method at all, just set the variant and name our view templates properly.

The only drawback of using Action Pack variants is one of style. Instead of the web templates being grouped in a directory they will all have +web format and live in the same controller directory as the natural templates. However, we don’t anticipating creating more than a few custom templates so the view directory shouldn’t get too crowded. In any case, this style difference is a small price to pay to conform to a standard Rails way of rending custom template. We’re ditching MultipleTemplates in favor of Action Pack variants.

 
6
Kudos
 
6
Kudos

Now read this

How to Structure and Render Views in Backbone

tl;dr Limited coupling between parent and child views is accomplished by making child views responsible for rendering their own content. Parent views communicate with child views by triggering collection events. Memory leaks are avoided... Continue →