Relative Timestamps in Rails

Facebook’s news feed popularized the relative timestamp format of “X hours ago”. For good reason too…why print an absolute timestamp so that people have to do the relative calculation in their head? It’s one less step for the user, and to be fair, pretty easy to implement.

Rails helpers to the rescue

Rails has a view helper aptly named time_ago_in_words. If you use the created_at attribute from a record, you could easily reference a relative timestamp from a corresponding view:

# app/views/posts/show.html.erb

Posted: <%= time_ago_in_words(post.created_at) %> ago

That’s great! But not enough…what happens when a user stays on the page for 10 min. and the latest post was no longer posted “1 minute ago”? Asking them to refresh the page every 10 minutes is no longer an acceptable answer.

Additionally, Rails 4 encourages the use of Russian doll caching, so if you cached record and use time_ago_in_words, the relative time of the post would never change. No bueno!

Fortunately, we can do better…

The Solution

Timeago.js is a jQuery plugin that converts timestamps to a relative format. They also boast the following on their website:

  • Avoid timestamps dated “1 minute ago” even though the page was opened 10 minutes ago; timeago refreshes automatically

  • You can take full advantage of page caching in your web applications, because the timestamps aren’t calculated on the server

  • You get to use microformats like the cool kids

The Implementation

  1. Download the plugin and place it in vendor/javascripts/timeago.jquery.js

  2. Add the following line to your application.js manifest file, so it’s picked up by the asset pipeline:

     //= require jquery.timeago
    
  3. Create a helper that you can use from your views that will do the dirty work for you:

     # app/helpers/time.rb
     module TimeHelper
       def timeago(time, options = {})
         options[:class] ||= "timeago"
         content_tag(
           :time,
           time.to_s,
           options.merge(datetime: time.getutc.iso8601)
         ) if time
       end
     end
    
  4. Reference the new helper method from your view - passing in the time attribute of the model:

     # app/views/posts/show.html.erb
     Posted: <%= timeago(post.created_at) %>
    

    This generates the following HTML tag:

     <time class="timeago" datetime="2013-11-08T20:05:37Z"></time>
    

    Now that timeago.js is loaded and you have the right HTML tags on the page, you need to invoke the plugin and let it do its thing.

  5. Add the following to the bottom of your layout and reload the page:

     # app/views/layouts/application.html.erb
     <body>
       <%= javascript_tag do %>
         $(function() {
           $("time.timeago").timeago();
         });
       <% end %>
     </body>
    

    Once the timeago() function is called, the timestamp above will look like:

     <time class="timeago" datetime="2013-11-08T20:05:37Z"
     title="2013-11-08 20:05:37 UTC">2 days ago</time>
    

And that’s it…sit on the page long enough and watch the timestamps increment. You’ve now got yourself a solution that is dynamic and allows you to cache the views until the cows come home!

Summary

Even though Rails has a simple mechanism for displaying relative timestamps in views, moving this functionality to the client side makes sense.

Another benefit you get is timezone interpretation. Even though we print the UTC time in the HTML tag, the plugin will detect the local timezone from the browser and adjust accordingly. In my experience, timezones are a huge pain and the more you can offload them to a solution like this, the better.

Happy time-stamping!

Note: Since writing this, I discovered the local_time gem from 37Signals. They’ve been a big advocate of moving relative timestamp calculation to the client-side. Their gem uses moment.js instead of timago.js and it includes code to update the timestamps if Turbolinks is being used. It’s worth checking out if you do this on a regular basis or use the moment.js library for other reasons.

P.S. In my book, Build a Ruby Gem, I cover how to create a Rails engine gem with the above functionality.