November 4, 2016 · rails erb ruby

Rendering Ruby objects in Rails

The render method in Rails reminds me of components in modern Javascript frameworks. A component consists of two parts: an object and a template. Any logic needed in the template is added to the object. The template then focuses solely on displaying the data and not thinking.

In Rails, a model can be passed to the render method. The partial is automatically found and the model object made available in the template. This creates component-like behavior for composing views.

Rendering ActiveRecord models

Consider an application that shows coffee shops in Seattle. Each shop has a Location record with a name and address.

The locations are fetched from the database in the controller and assigned to @locations.

# apps/controllers/locations_controller.rb
class LocationsController < ApplicationController
  def index
    @locations = Location.all
  end
end

The @locations are then passed directly to render and Rails conventions take over.

<%# app/views/locations/index.html.erb %>
<%= render @locations %>

Rendering @locations informs you: Missing partial locations/_location. Rails attempts to render a partial based on the name of the Location model.

Adding the location partial lets everything work as expected.

<%# app/views/locations/_location.html.erb %>
<%= location.name %>

Since render received an array of locations, the location partial will be rendered once for each location. Inside the partial, a location variable is made available similar to an each loop.

Adding logic to the views

New coffee shops pop up all the time. It would be useful for users to see which shops were added recently. Let’s add a “NEW” label to any location added within the past thirty days. This way the hipsters know where to gather.

<%# app/views/locations/_location.html.erb %>
<%= location.name %>
<% if location.created_at > 30.days.ago %>
  <span class="label">NEW</span>
<% end %>

At this point, things work as expected. All recently added locations are tagged with “NEW” and the java junkies are at bay. However, the view knows too much about the model. It knows more about what it means to be a recent location than a location does!

This can be cleaned up by moving the definition of a recent location to the model instead of the view.

# app/models/location.rb
class Location < ActiveRecord::Base
  def added_recently?
    created_at > 30.days.ago
  end
end

Now the view can ask the object if it was recently added instead of trying to figure it out itself.

<%# app/views/locations/_location.html.erb %>
<%= location.name %>
<% if location.added_recently? %>
  <span class="label">New</span>
<% end %>

Rendering plain Ruby objects in Rails

Rails has made it really easy to render classes inheriting from ActiveRecord. Magic is in place so rendering just works. However, plain Ruby objects require a little bit of pixie dust.

Consider a page footer. It could be dropped straight into the application layout, but where does its logic belong? It could be placed in ApplicationController or ApplicationHelper, but that would clutter the global namespace.

A clean solution is to encapsulate the footer logic in a Footer class.

# app/models/footer.rb
class Footer
  def copyright
    "&copy; #{copyright_years}. All rights reserved.".html_safe
  end

  private

  def copyright_years
    [2012, Today.today.year].join(" – ")
  end
end

It can then be rendered in the application layout using the render method.

<%# app/views/layouts/application.html.erb %>
<%= render Footer.new %>

Uh oh. That throws an error: '#<Footer:0x007fdc38496d28>' is not an ActiveModel-compatible object. It must implement :to_partial_path. Rails does not know which partial to render.

This can be fixed by including ActiveModel::Conversion. The Conversion module maps class names to partials.

# app/models/footer.rb
class Footer
  include ActiveModel::Conversion
  ...
end

Alternatively, the to_partial_path method could have been defined on Footer to return a partial name. However, including the Conversion module ensures consistent partial naming with other ActiveRecord objects in the application.

After including ActiveModel::Conversion there is a different error: Missing partial footers/_footer. Rails now knows how to render the Footer class, but the partial does not yet exist.

Let’s add a simple footer partial that displays the copyright.

<!-- app/views/footers/_footer.html.erb -->
<%= footer.copyright %>

That’s all there is to it. The Footer is now a renderable plain old Ruby object that encapsulates logic and renders a template.