ruby erb javascript 87 lines · 3 tabs

Time-ago formatting with Stimulus (no heavy date libs)

Shared by codesnips Jan 2026
3 tabs
module TimeAgoHelper
  def time_ago_tag(time, locale: I18n.locale, **options)
    return if time.blank?

    time = time.to_time
    fallback = l(time, format: :short)

    content_tag(
      :time,
      fallback,
      datetime: time.iso8601,
      title: l(time, format: :long),
      data: {
        controller: "time-ago",
        time_ago_datetime_value: time.iso8601,
        time_ago_locale_value: locale.to_s
      },
      **options
    )
  end
end
3 files · ruby, erb, javascript Explain with highlit

This snippet builds relative timestamps ("3 minutes ago", "2 days ago") that update themselves in the browser without pulling in Moment.js or date-fns. The whole thing leans on Intl.RelativeTimeFormat, a native browser API that handles locale-aware pluralization and phrasing for free, so the client-side footprint stays near zero.

The time_ago_tag helper renders a semantic <time> element with a machine-readable datetime attribute in ISO 8601. That attribute is the single source of truth: the server never guesses what "ago" means at render time, it just emits the absolute instant. The element carries data-controller="time-ago" and a data-time-ago-datetime-value so the Stimulus controller can hydrate it. Crucially, the tag also renders a human-readable fallback inside the element, so if JavaScript is disabled or slow the reader still sees a real date — this is progressive enhancement rather than a blank node.

In time_ago_controller.js, the datetimeValue typed value auto-parses the ISO string into a Date. On connect() the controller renders once and then starts an interval. The refresh() method computes the delta and calls format(), which walks a small table of thresholds (minute, hour, day, week, month, year) and picks the largest unit that fits, passing a negative value to RelativeTimeFormat so the API produces past-tense phrasing.

Two details make it production-safe. The interval cadence adapts in scheduleNext(): very recent times refresh every few seconds, older ones every minute, and anything past a day stops ticking entirely since "5 days ago" does not need per-second churn. And disconnect() clears the timer, which matters under Turbo where controllers connect and disconnect as frames swap — without cleanup, orphaned intervals would leak and multiply.

The controller also sets the element's title to the full localized timestamp so hovering reveals the exact time, keeping both quick-scan and precise readings available. The trade-off is that clock skew between server and client can make brand-new items read as "in a few seconds"; the Math.min(delta, 0) guard clamps that so future-ish drift still shows "just now" rather than a confusing future phrase.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Semantic HTML Example</title>

Semantic HTML5 elements and accessibility best practices

html html5 semantics
by Alex Chang 2 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Time-ago formatting with Stimulus (no heavy date libs) — share card
Link copied