Rails 4 Request Variants

We explore the use of new request variants introduced in Rails 4 to render different layouts and views for mobile users.

Setting a Request Variant

Request variants in themselves are nothing more than a way to further distinguish your views and responses during rendering. A common example, and one that we'll cover here, is rendering different views for mobile versus desktop.

A request may have multiple variants, and you set the variant for a request simply enough. Below I make use of the excellent browser gem.

class ApplicationController < ActionController::Base
  before_action :set_request_variant

  private def set_request_variant
    request.variant = :mobile if browser.mobile?
  end
end

If you had reason, say to see if you were responding to a variant, you might do the following.

class ApplicationController < ActionController::Base
  helper_method :mobile_request?

  private def mobile_request?
    # variant is nil until you set it, but then is Array
    Array(request.variant).include? :mobile
  end
end

Responding

Variants are useful because they allow you even finer grain control over your responses. Our simplest example is a controller action responding to both mobile and desktop HTML.

class ProductsController < ApplicationController
  def index
  end
end

Rails responds with HTML by default, so assuming you have two view files for each variant, the above will just work (more on what the file names are to come).

Different Responses

Part of the power of variants lie in the ability to further refine behavior, even for the same mime type response.

class ProductsController < ApplicationController
  def index
    respond_to do |format|
      format.html.mobile do
        # do something specific for mobile variant
      end
    end
  end
end

Here's an example showing a different nesting style.

class ProductsController < ApplicationController
  def index
    respond_to do |format|
      format.html do |html|
        html.mobile { redirect_to root_path }
      end
    end
  end
end

Finally, you might find use for the following if you want to customize the response without a variant and with.

class ProductsController < ApplicationController
  def index
    respond_to do |format|
      format.html do |html|
        html.any { render layout: 'simple' }
        html.mobile { redirect_to root_path }
      end
    end
  end
end

Responding

Rails uses a file naming convention to map variants to view files, and if the file exists it will be used to render the response.

The view files for our above action would be named the following, using the convention of a "+" with the variant in the filename after the mime type extension.

app/views/products/index.html.haml
app/views/products/index.html+mobile.haml

You could certainly expand this and find it useful in rendering your JS responses as well.

app/views/products/index.js+mobile.erb

Layouts

You can easily use variants in more than just your views or partials, layouts are fair game.

app/views/layouts/application.html.haml
app/views/layouts/application.html+mobile.haml

If the request variant is set to mobile, then the mobile layout will automatically be used. Otherwise the default layout is used as a fallback.

Note that the file and for a layout is evidently different than for a view here, the +mobile comes after the .html.

In the project I worked on, I had a few controllers that I absolutely did not want to render the mobile layout, even if it was requested on a mobile device. Since we just used a before filter in our controller to set the variant, explicitly skipping that before filter per controller removes the variant and skips the mobile layout.

class ProductsController < ApplicationController
  skip_before_action :set_request_variant
end

Because I'm a big fan of code reuse across projects and organization, I refactored all of this out into a controller concern.

module MobileVariants
  extend ActiveSupport::Concern

  included do
    before_action :determine_mobile_variant
    helper_method :mobile_request?
  end

  module ClassMethods
    def skip_mobile_variant(*args)
      skip_before_action :determine_mobile_variant, *args
    end
  end

  private def determine_mobile_variant
    request.variant = :mobile if browser.mobile?
  end

  private def mobile_request?
    Array(request.variant).include? :mobile
  end
end

Using this concern is simplistic and shows clear intention.

class ApplicationController < ActionController::Base
  include MobileVariants
end

class ProductsController < ApplicationController
  skip_mobile_variant only: :edit
end

Wrap Up

Variants are just one of the major new features introduced in Rails 4.1, but I think will be a major staple of any Rails developer toolkit moving forward.

Our Products

It takes one to know one - we've walked the walk by building our own products that customers love.

Ready to have a chat?

Contact us to chat with our founder
so we can learn about you and your project.