Display localised date / time information from Phoenix LiveView

Danny Hawkins
Quiqup Engineering
Published in
4 min readApr 13, 2022

I recently hit a snag moving away from JS heavy frontends to Phoenix LiveView. There are a few things one might take for granted, and it’s only when you try to do that thing in a new technology that you appreciate the simple method you had before. In this case it was giving the user a time back in their local timezone (and preferred locale settings)

I’m a strong advocate that all date/time information in the database and the code base should stay in UTC until it needs to be returned / presented to the user.

I won’t go into the whys, but I found a good article that summarises it pretty well.

When creating API’s I will always be returning UTC eg “2022–04–14T22:00:00Z”, typically when javascript frontend decide to display that date they will use something like:

new Date('2022-04-13T12:00:00Z').toLocaleString()// Output is specific to the user locale (I'm UTC+4 in GST)
'13/04/2022, 16:00:00'

This is super useful, because it means we are using a friendly time format for data, but giving the user something that makes sense to them.

The Problem

When using phoenix LiveView, there is no direct way to get back the locale of the user from the browser, and use it to format the time in the HEEX / HTML template (or if there is it eluded me, or was flaky).

Somehow we need to be able to leverage the local javascript functionality to be able to translate a UTC date into a locale friendly date.

The Solution

The way I found to solve this was using AlpineJS on the front end, and create a live helper / web component to help with the conversion. Assuming you have a recent phoenix live view application already setup, let’s go step by step.

Step 1: Install Alpine

First of all we need to add alpine, move into the assets folder and install the npm package

cd /assets
npm install alpinejs

Next update assets/js/app.js to include alpine

import 'phoenix_html'import Alpine from 'alpinejs' // Import Alpine// Establish Phoenix Socket and LiveView configuration.import { Socket } from 'phoenix'
import { LiveSocket } from 'phoenix_live_view'
import topbar from '../vendor/topbar'
// Add alpine to window and start
window.Alpine = Alpine
Alpine.start()

Also we want alpine values to survive a LV update, so update the LiveSocket creation, adding the whole section for the dom key

let liveSocket = new LiveSocket('/live', Socket, {
params: { _csrf_token: csrfToken },
dom: {
onBeforeElUpdated(from, to) {
if (from.__x) {
window.Alpine.clone(from.__x, to)
}
},
},

})

This next change is not well documented, and had me scratching my head for a couple of hours, you need to make sure the the esbuild target is es2019 or later, in config.exs

config :esbuild,
version: "0.12.18",
default: [
args:
~w(js/app.js --bundle --target=es2019 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
cd: Path.expand("../assets", __DIR__),
env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
]

OK, so thats alipine all good to go

Step 2: Create the datetime helper function

Create a new module (or update an existing one) to include the datetime helper. Mine looks like this

Then make sure to include this helper in the view helpers block in your web module root (in my case QuiqupFleetWeb)

defp view_helpers do
quote do
use Phoenix.HTML

import Phoenix.LiveView.Helpers
import Phoenix.View
import QuiqupFleetWeb.Gettext
# Import the live helpers module here
import QuiqupFleetWeb.LiveHelpers
import Phoenix.LiveView.Helpers
alias QuiqupFleetWeb.Router.Helpers, as: Routes
end
end

And thats it, now in your view you are able to just call the web component, and it will automatically replace empty inner text with locale specific date time. For me using GST (Gulf Time) +4hrs UTC

<.datetime id="time" value={~U[2022-04-14 18:00:00Z]} format="timeshort" />Outputs: 22:00<.datetime id="full-date" value={~U[2022-04-14 18:00:00Z]} format="datetime" />Outputs: 14/4/2022 22:00:00

Breakdown

The main thing we are using the helper for is to return a span, and on that span we are setting alpinejs x-data to a date in js, datetime being passed in via assigns as value

x-data={"{date: new Date('#{datetime}')}"}

Then we are using alpines x-text to tell alpine what to replace the inner text of the element with

x-text={"date.#{locale_fn}"}

The locale_fn is just describing the function to call against date in javascript, for which I have made some predefined formats that I want

case format do
"datetime" -> "toLocaleString([],{dateStyle: 'short', timeStyle: 'short'})"
"date" -> "toLocaleDateString()"
"timeshort" -> "toLocaleTimeString([],{timeStyle: 'short'})"
"time" -> "toLocaleTimeString()"
_ -> "toLocaleString()"
end

and thats it!

EDIT 18th April 2020

My first version of this was without the phx-update="ignore" attribute on the date span, turns out the LV and alpine don’t play super well right now, so if you don’t ignore the phx update the text can get wiped out. I’ll think about a more elegant solution, but for my current needs this gets over the issue.

--

--

Danny Hawkins
Quiqup Engineering

I’m a CTO and Co-Founder of a company called Quiqup, a fan of clean architecture and code, and Golang my language of choice.