Render templates from anywhere in Ruby on Rails

Published Aug 19, 2017Last updated Dec 17, 2017

This post originally appeared on my Svbtle-hosted blog here.

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.

Discover and read more posts from Corey Ward
get started