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})
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.