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.