Cleaning up rails view logic using Draper
Rails makes it super easy to create database backed applications fast, but sometimes we get sloppy in all the hurry.
First iteration (quick and dirty)
Assume we have a simple blog application (yeah, original, I know) with a BlogPost model. The BlogPost model has three properties: title, content and author, all of which are strings. Our scaffolded index view looks like this:
<% @blog_posts.each do |blog_post| %>
<tr>
<td><%= blog_post.title %></td>
<td><%= blog_post.content %></td>
<td><%= blog_post.author %></td>
... other code to display in each line
</tr>
<% end %>
We now want to display when the post was published, and our first stab at it looks like this:
<td><%= blog_post.created_at %></td>
<% @blog_posts.each do |blog_post| %>
<tr>
... previous lines
<td><%= blog_post.created_at.strftime("%m.%b.%Y") %></td>
... other code to display in each line
</tr>
We also want to show the published time when viewing an individual post, but this time we want to include the time (hours and minutes only).
<%= blog_post.created_at.strftime("%m.%b.%Y %H:%M") %>
Now the timestamps are displayed just the way we want them.
Iteration 2: reducing view logic
The problem with the above approach is that we now have logic in our view. Not only does this clutter the view code, but if we want to use the same date format for blog posts elsewhere in the app, we have to duplicate this code everywhere.
Our first attempt to clean this up is to add a new method to the BlogPost model which we can then call instead of #created_at:
class BlogPost < ActiveRecord::Base
def short_date
created_at.strftime("%m.%b.%Y")
end
def long_date
created_at.strftime("%m.%b.%Y %H:%M")
end
end
Which allows us to change the index view to:
<td><%= blog_post.short_date %></td>
and our show view to:
<p><%= @blog_post.long_date %></p>
This looks much better.
Third iteration: remove presentation logic from model
While our views are certainly cleaner, we just added view specific logic to our model. The model should not be concerned about how to format data for particular views. So, where then do we put it? The answer is Decorators.
For this I will rely on one of my favourite gems, namely Draper.
We'll create a new decorator, simply called BlogPostDecorator. This will be our place for view specific logic.
class BlogPostDecorator < Draper::Decorator
delegate_all
def short_date
object.created_at.strftime("%m.%b.%Y")
end
def long_date
object.created_at.strftime("%m.%b.%Y %H:%M")
end
end
To make this available in the views, we must decorate the objects returned by the controller:
def index
@blog_posts = BlogPost.all.decorate
end
def show
@blog_post = BlogPost.find(params[:id]).decorate
end
Conclusion
We have met our view requirements with cleaner views and cleaner models by using decorators. I have merely shown you a glimpse of what Draper is capable of and I encourage you to head over to the Draper Github page and look through it.
If you want to play around with this, you can take a look at the source code for the BlogPost app.