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
"© #{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.