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.