Skip to content

Commit

Permalink
Add request.variant API and guides documentation
Browse files Browse the repository at this point in the history
Add prose and code samples for:

* `request.variant=`
* `request.variant`

Add sections to the Action View and Action Controller guides, along with
some code samples.

The majority of these changes were excised from past pull requests, such
as [rails#12977][] and [rails#18939][].

[rails#12977]: rails#12977
[rails#18939]: rails#18939

Co-authored-by: zzak <zzakscott@gmail.com>
  • Loading branch information
seanpdoyle and zzak committed Dec 7, 2023
1 parent c5a8621 commit 587b07c
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 3 deletions.
57 changes: 56 additions & 1 deletion actionpack/lib/action_dispatch/http/mime_negotiation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,50 @@ def formats
end
end

# Sets the \variant for template.
# Sets the \variant for the response template.
#
# When determining which template to render, Action View will incorporate
# all variants from the request. For example, if an
# <tt>ArticlesController#index</tt> action needs to respond to
# <tt>request.variant = [:ios, :turbo_native]</tt>, it will render the
# first template file it can find in the following list:
#
# - app/views/articles/index.html+ios.erb
# - app/views/articles/index.html+turbo_native.erb
# - app/views/articles/index.html.erb
#
# Variants add context to the requests that views render appropriately.
# Variant names are arbitrary, and can communicate anything from the
# request's platform (`:anrdoid`, `:ios`, `:linux`, `:macos`, `:windows`)
# to its browser (`:chrome`, `:edge`, :firefox`, `:safari`), to the type
# of user (`:admin`, `:guest`, `:user`).
#
# Be judicious when adding new variant templates, as introducing similar,
# but separate template files can make maintaining your view code more
# difficult.
#
# ==== Parameters
#
# * +variant+ - a symbol name or an array of symbol names for variants
# used to render the response template
#
# ==== Examples
#
# class ApplicationController < ActionController::Base
# before_action :determine_variants
#
# private
# def determine_variants
# variants = []
#
# # some code to determine the variant(s) to use
#
# variants << :ios if request.user_agent.include?("iOS")
# variants << :turbo_native if request.user_agent.include?("Turbo Native")
#
# request.variant = variants
# end
# end
def variant=(variant)
variant = Array(variant)

Expand All @@ -95,6 +138,18 @@ def variant=(variant)
end
end

# Returns the \variant for the response template as an instance of
# ActiveSupport::ArrayInquirer.
#
# request.variant = :phone
# request.variant.phone? # => true
# request.variant.tablet? # => false
#
# request.variant = [:phone, :tablet]
# request.variant.phone? # => true
# request.variant.desktop? # => false
# request.variant.any?(:phone, :desktop) # => true
# request.variant.any?(:desktop, :watch) # => false
def variant
@variant ||= ActiveSupport::ArrayInquirer.new
end
Expand Down
56 changes: 55 additions & 1 deletion guides/source/action_controller_overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,61 @@ Rails collects all of the parameters sent along with the request in the `params`
[`query_parameters`]: https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-query_parameters
[`request_parameters`]: https://api.rubyonrails.org/classes/ActionDispatch/Request.html#method-i-request_parameters

#### `request.variant`

Controllers might need to tailor a response based on context-specific information in a request. For example, controllers responding to requests from a mobile platform might need to render a HTML, JSON, or XML different content than requests from a desktop browser. They can add that context to the requests variant so that views render appropriately. Variant names are arbitrary, and can communicate anything from the request's platform (`:anrdoid`, `:ios`, `:linux`, `:macos`, `:windows`) to its browser (`:chrome`, `:edge`, `:firefox`, `:safari`), to the type of user (`:admin`, `:guest`, `:user`).

You can set the [`request.variant`](https://api.rubyonrails.org/classes/ActionDispatch/Http/MimeNegotiation.html#method-i-variant-3D) in a `before_action`:

```ruby
request.variant = :tablet if request.user_agent.include?("iPad")
```

Responding with a variant in a controller action is like responding with a format:

```ruby
# app/controllers/projects_controller.rb

def show
# ...
respond_to do |format|
format.html do |html|
html.tablet # renders app/views/projects/show.html+tablet.erb
html.phone { extra_setup; render } # renders app/views/projects/show.html+phone.erb
end
end
end
```

Provide separate templates for each format and variant:

* `app/views/projects/show.html.erb`
* `app/views/projects/show.html+tablet.erb`
* `app/views/projects/show.html+phone.erb`

You can also simplify the variants definition using the inline syntax:

```ruby
respond_to do |format|
format.html.tablet
format.html.phone { extra_setup; render }
end
```

The [`request.variant`](https://api.rubyonrails.org/classes/ActionDispatch/Http/MimeNegotiation.html#method-i-variant) method returns an instance of [`ActiveSupport::ArrayInquirer`](https://api.rubyonrails.org/classes/ActiveSupport/ArrayInquirer.html):

```ruby
request.variant = :phone
request.variant.phone? # => true
request.variant.tablet? # => false

request.variant = [:phone, :tablet]
request.variant.phone? # => true
request.variant.desktop? # => false
request.variant.any?(:phone, :desktop) # => true
request.variant.any?(:desktop, :watch) # => false
```

### The `response` Object

The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. To get a full list of the available methods, refer to the [Rails API documentation](https://api.rubyonrails.org/classes/ActionDispatch/Response.html) and [Rack Documentation](https://www.rubydoc.info/github/rack/rack/Rack/Response).
Expand Down Expand Up @@ -1312,4 +1367,3 @@ The health check will now be accessible via the `/healthz` path.
NOTE: This endpoint does not reflect the status of all of your application's dependencies, such as the database or redis cluster. Replace "rails/health#show" with your own controller action if you have application specific needs.

Think carefully about what you want to check as it can lead to situations where your application is being restarted due to a third-party service going bad. Ideally, you should design your application to handle those outages gracefully.

6 changes: 5 additions & 1 deletion guides/source/layouts_and_rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ With this set of variants Rails will look for the following set of templates and

If a template with the specified format does not exist an `ActionView::MissingTemplate` error is raised.

Instead of setting the variant on the render call you may also set it on the request object in your controller action.
Instead of setting the variant on the render call you may also set [`request.variant`](https://api.rubyonrails.org/classes/ActionDispatch/Http/MimeNegotiation.html#method-i-variant-3D) in your controller action. Learn more about variants in the [Action Controller Overview](./action_controller_overview.html#request-variant) guides.

```ruby
def index
Expand All @@ -465,6 +465,10 @@ end
end
```

NOTE: Be stingy when adding new variants. Only reach for variants when groups
of conditionals (that share a theme) or other more traditional templating
techniques become unmanageable.

#### Finding Layouts

To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions.
Expand Down

0 comments on commit 587b07c

Please sign in to comment.