Removing Friction by Converting Datetimes to Local & Relative Using Rails & Javascript

Jan 16, 2021 • Ben Sochar

Lets pretend We’re in LA & looking at an online event in NYC that takes place on Jan 24 at 6:30pm. We see this date:

Saturday, Jan 24 6:30 PM EDT

Now you’ve got to figure if you can go - how far ahead is NY?, Is today Friday?. We can remove some friction by making the dates nicer. Like these:

Today at 3:30 PM PST Tomorrow at 3:30 PM PST Saturday at 3:30 PM PST

Basecamp had a gem that did this but it hasn’t been maintained in years. So we’ll make our own with a helper & some javascript.

This method uses javascript to localize a datetime client side so we reduce DB calls & makes views easier to cache.

When we’re done, we’ll have this HTML:

<time class="js-local-time transition-all" datetime="2021-01-16T18:30:00-04:00" data-dateformat="iiii, MMM d, yyyy h:mm a z" data-datevalue="&quot;2021-01-16T18:30:00-04:00&quot;">Today at 6:30 PM EDT</time>

You’ll need to update your time format config file. Or add it if you don’t have it - it makes life so much easier. We need a format that date-fns can parse that will match dates parsed with Ruby.

# config/initializers/time_formats.rb

Date::DATE_FORMATS[:event_time] = '%A, %b %e %l:%M %p %Z' # Monday, Jan 1 12:00 PM EDT
Time::DATE_FORMATS[:event_time] = '%A, %b %e %l:%M %p %Z' # Monday, Jan 1 12:00 PM EDT

Date::DATE_FORMATS[:event_date_js] = 'iiii, MMM d, yyyy h:mm a z' 
Time::DATE_FORMATS[:event_time_js] = 'iiii, MMM d, yyyy h:mm a z'

We’ll add a helper. The CSS classes & “spinner” are optional. I thought it make someone crazy if they see a date change a split second after navigating. We’ll pull in our Date format from our config, convert it to iso8601 & to JSON. We can use data attributes on the time element to modify the output.

# app/helpers/date_time_helper.rb

module DateTimeHelper
  def format_date_time_to_local(content, time, format = Date::DATE_FORMATS[:event_date_js])
    spinner = spinner_border('text-muted spinner-border-sm ms-1')
    content_tag(
      :time,
      class: 'js-local-time text-muted transition-all',
      datetime: time.iso8601,
      data: { 
        dateformat: format, 
        datevalue: time.iso8601.to_json
      }
    ) do 
      content.concat(spinner).html_safe
    end
  end
end

Yarn or NPM date-fns. I only needed US format but date-fns supports others. We’ll also import realtive formatting & checks for same year/week as the current date so we can cleanup the date.

There’s the optonal removal of the “text-muted” styles once the formatting is complete.

// frontend/src/javascripts/dateTimeHelper.js

'use strict'
export const supportedLocales = ['en-US']
import { format } from 'date-fns-tz'
import { isValid, formatRelative, isThisWeek, isThisYear, getDay } from 'date-fns'

export default function dateTimeHelper (ele) {
  let str = ''
  const nowDate = new Date()
  const eleData = ele.dataset
  let dateFormat = eleData.dateformat
  const dateVal = Date.parse(JSON.parse(eleData.datevalue))

  // We don't need to know the year if it's the same as the current year
  let dateFormatWithYear = function(format) {
    if (isThisYear(dateVal)) {
      return dateFormat.replace('yyyy', '')
    } else {
      return format
    }
  }

  if (isValid(dateVal)) {
    const offsetWeekDay = getDay(nowDate)
    // We can drop the day of the week if it's the same as the current week
    if (isThisWeek(dateVal, { weekStartsOn: offsetWeekDay })) {
      let realtiveStr = formatRelative(dateVal, nowDate, { weekStartsOn: offsetWeekDay }) + ' ' + format(dateVal, 'z')
      str = realtiveStr.replace(/^\w/, (c) => c.toUpperCase())

    } else {
      str = format(dateVal, dateFormatWithYear(dateFormat))
    }
    ele.textContent = str
    ele.classList.remove('text-muted')
  } else {
    ele.classList.remove('text-muted')
  }
}

document.addEventListener('turbolinks:load', () => {
  const eles = document.querySelectorAll('.js-local-time')
  Array.prototype.forEach.call(eles, function(ele) {
    dateTimeHelper(ele)
  })
})

date-fns, dates, javascript, & rails