Render templates from anywhere in Ruby on Rails
Rails 5 recently shipped, and among many other new features is a new renderer that makes it easy to render fully composed views outside of your controllers.
This comes in handy if you want to attach, say, an HTML receipt to an order confirmation email, or render an HTML template for wkhtmltopdf to convert to PDF.
Previously it was a chore to render views with their full context. You needed to emulate a request and a controller instance, and keep track of the relatively intricate suite of methods and their signatures. The new Rails 5 renderer provides a handful of intuitive interfaces right on the ActionController::Base
class that are inherited by your ApplicationController
et al.
Here’s an example:
plain_html = ApplicationController.render('users/show')
That’s all that’s needed for a simple view. Helpers that are available in ApplicationController
are available to your view, including controller-level helper methods that you might have (like current_user
).
You can pass additional information almost exactly how you would render from a controller:
user = User.find(params[:id])
UsersController.render 'users/show', assigns: { user: user }
# or
renderer = ApplicationController.renderer
renderer.defaults[:https] = true
renderer.render inline: "<% request.ssl? %>" # => 'true'
# or
renderer = PostsController.renderer.new method: 'post'
renderer.render template: 'posts/show', layout: false
All .render
methods return a fully rendered string. This makes it easy to build plain-old-ruby-objects (PORO) that are responsible for composition and rendering:
class UserCard
attr_reader :user
def initialize(user)
@user = user
end
def teams
user.teams.where(active: true).order(score: :desc).first(5)
end
def current_score
teams.map(&:score).reduce(:+)
end
def render
renderer.render template: 'users/_card',
assigns: { user: user },
locals: { current_score: current_score, teams: teams }
end
protected
def renderer
ApplicationController.renderer
end
end
# app/controllers/users_controller.rb
def show
@user = User.find params[:id]
end
# app/views/users/show.html.erb
<% UserCard.new(@user).render %>
# app/views/users/_card.html.erb
<aside class="user-card">
<div class="user-card--name"><%= @user.name %></div>
<div class="user-card--score"><%= current_score %></div>
<div class="user-card--teams">
<%= teams.map(&:name).to_sentence %>
</div>
</aside>
In the above example, the controller isn’t responsible for the logic around what information gets rendered on a UserCard
, and the show
view doesn’t have to know that _card
expects a current score. This example is pretty contrived, but with a little polymorphism and a more demanding spec, this approach can yield great results without introducing a complex chain with full presenters and decorators.
Another common use case is sending emails. ActionMailer already supports rendering within methods, but doing more than rendering an HTML template can add clutter and unnecessary coupling. Here’s a real world use case from a project I’ve been working on:
A client I’ve been working with needed to email an executed (signed) legal agreement to users after they signed it, in PDF format. My mailer looks like this:
class UserMailer < ApplicationMailer
def executed_agreement(user)
@user = user
attachments['Executed Agreement.pdf'] =
ExecutedAgreement.new(user).to_pdf
mail to: @user.email, subject: 'Executed Agreement'
end
end
class ExecutedAgreement
def initialize(user)
@user = user
end
def to_pdf
WickedPdf.new.pdf_from_string(to_html)
end
def to_html
ApplicationController.render(
template: 'agreements/show',
layout: false,
assigns: { user: @user }
)
end
end
We recently switched out the PDF rendering tooling to move from Prawn (which is fast and works well) to WickedPDF (which allows us to match the web styling) without touching the Mailer at all. The view also moved to a new controller and the variables used in the view changed to allow admins to view agreements for any users, but UserMailer
has gone untouched and the code remains as clear in its intent and expression as it was when it was written.
You can learn more about the options available for the new Rails 5 renderer here and check out the changelog that added this feature for a full review of what’s new.
Cheers!