Formatting the user's local date-times using Hooks

Image by Annie Ruygt

Problem

When we’re developing an application for users around the world, we are bound to hit problems with timezones.

Often we decide to store the dates in UTC format to avoid storing the timezone of each user. This brings us to the real problem, “How can we display the UTC time in the user’s local timezone?”

Solution

We need to convert the date to the user’s local timezone and display it. We can use the user’s locale information in the browser, add a few lines of Javascript in a Client Hook and update the DOM to display the right time!

Converting a UTC date to a local date in Javascript

First we’ll create a Date object, passing our UTC date as a parameter to the constructor. This lets us use the Date class methods to manipulate dates.

let dt = new Date("2016-05-24T13:26:08.003Z");

Through a Date class object we can use the toLocaleString method. Based on the user’s default locale and timezone, this method returns the local date as a string.

dt.toLocaleString()
//   5/24/2016, 8:26:08 AM

We can also extract and display the timezone that is used internally for the conversion as follows:

Intl.DateTimeFormat().resolvedOptions().timeZone;
//   America/Mexico_City

Concatenating the results of the previous functions, we put our string into a more friendly format:

let dt = new Date("2016-05-24T13:26:08.003Z");
let dateString = dt.toLocaleString() +
                 " " + 
                 Intl.DateTimeFormat().resolvedOptions().timeZone;

//    5/24/2016, 8:26:08 AM America/Mexico_City

Configuring and defining a Hook

We’ve figured out how to get a new representation of a date from the user locale with JavaScript. Now we want to show the date in its new format from our LiveView application.

We’ve stored the date we want to show in the @utc assign, and we render its content inside a time tag:

<time><%= @utc %></time>

To change this to the new format, we’ll use a client hook to execute a variant of the lines of Javascript we used above.

First we need to define our Hooks object inside assets/js/app.js and add the hook, which we name LocalTime . This hook is responsible for taking the content of the time tag, reformatting it and updating its content.

let Hooks = {}

Hooks.LocalTime = {
  mounted() {
    let dt = new Date(this.el.textContent);
    this.el.textContent = 
      dt.toLocaleString() + 
      " " + 
      Intl.DateTimeFormat().resolvedOptions().timeZone;
  }
}

A hook can be executed at different life stages of an HTML element. Above, we defined the mounted callback, so the transformation will happen when the time tag is added to the DOM and the component has finished mounting.

All callbacks in a hook object have in-scope access to the el attribute, which is a reference to the DOM element on which the hook is running: here, the time tag. We get the UTC date from its textContent attribute, and store it in a Date object called dt:

let dt = new Date(this.el.textContent);

Then we replace the content of the HTML element with the new string we’ve created.

this.el.textContent = dt.toLocaleString() + 
                      " " +
                      Intl.DateTimeFormat().resolvedOptions().timeZone;

The conversion also needs to be redone whenever the server updates the element, so we add the updated callback, with a small refactor to avoid duplicating code:

Hooks.LocalTime = {
  mounted(){
    this.updated()
  },
  updated() {
    let dt = new Date(this.el.textContent);
    this.el.textContent = 
      dt.toLocaleString() + 
      " " + 
      Intl.DateTimeFormat().resolvedOptions().timeZone;
  }
}

Now that our LocalTime hook is defined, we pass the Hooks object to the socket:

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks})

Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Phoenix app today!

Executing a Hook from an HTML element

The only missing piece is to tie our LocalTime hook to the time tag we defined. For this, we’ll use the phx-hook option, passing the name of our LocalTime hook.

<time phx-hook="LocalTime" id="my-local-time">
  <%= @utc %>
</time>

Note that, when using phx-hook, we must always define a unique DOM ID for the HTML element.

Let’s see our work in action (slowed down for visibility):

When the time tag is first rendered, the date is in UTC format, but then our hook is executed and the content of the tag is replaced.

We can make a little improvement and hide the initial content of the time tag using CSS. Here we’re using the TailwindCSS invisible class.

<time phx-hook="LocalTime" id="my-local-time" class="invisible">
  <%= @utc %>
</time>

This way the time tag still exists in the DOM, but its content will be hidden.

Once we’ve modified the date format, we remove the class with JavaScript to display the content of the time tag:

updated() {
    let dt = new Date(this.el.textContent);
    this.el.textContent = 
      dt.toLocaleString() +
      " " +
      Intl.DateTimeFormat().resolvedOptions().timeZone;
    this.el.classList.remove("invisible")
  }

Let’s see what has changed:

The content of the time tag is not displayed until the date is reformatted and replaced.

Creating a reusable component

If this is not the only date we’ll show in our application, we can go one step further and define a function component that takes a unique DOM ID and a date as part of its assigns:

def local_time(%{date: date, id: id} = assigns) do
  ~H"""
    <time phx-hook="LocalTime" id={@id} class="invisible">
      <%= @date %>
    </time>
  """
end

We use our component as follows:

<.local_time id="my-date" date="2016-05-24T13:26:08.003Z"/>

Or passing the content of an assign:

<.local_time id="my-date" date={@date}/>

Discussion

We could get the user’s locale and timezone from the browser and use server-side libraries like Timex to manipulate datetimes, which would involve writing lines of code on both, the client and server side, and storing those values in some part of our application.

With the Phoenix client Hooks, we can format dates using a few lines of Javascript and entirely client-side.