Conditional Custom Templates with Action Pack Variants
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
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
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
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.