Getting and Displaying the User’s Local Time in LiveView

A colorful world map with different markers of people across different countries.

Whether you need end-to-end product development support or the engineering know-how for a targeted project, we can help. Book a free consult to learn more.

In a Phoenix/LiveView application, it’s a bit of a challenge to display the current time to the user in the user’s time zone. There are two primary reasons for this:

  1. Elixir code runs server-side rather than client-side, so the current time in our app’s Elixir code is the time on the server where our app is running.
  2. By default, Elixir can only handle the Coordinated Universal Time (UTC) time zone.

However, with a time zone library and a little JavaScript, we can solve both problems.

Let’s start by making a Phoenix application with a LiveView implementation of a clock.

Making a Clock in LiveView

  1. Install elixir (instructions here).
  2. In your terminal, run mix phx.new --no-ecto my_clock_app. Hit Enter to fetch and install dependencies.
    • If you have a database server running, you can omit the --no-ecto flag if you want.
  3. Follow any instructions in the end of the output from mix phx.new my_clock_app, starting with cd my_clock_app.
  4. In the lib/my_clock_app_web/router.ex file, following the “scope "/", MyClockAppWeb do” block (not inside it), add this live_session block:
  scope "/", MyClockAppWeb do
    pipe_through :browser

    get "/", PageController, :home
  end
+
+ live_session :default do
+   scope "/", MyClockAppWeb do
+     pipe_through(:browser)
+
+     live("/clock", ClockLive)
+   end
+ end
  1. Finally, create our LiveView for the clock by creating the file lib/my_clock_app_web/live/clock_live.ex with this code:
defmodule MyClockAppWeb.ClockLive do
  use MyClockAppWeb, :live_view

  def mount(_params, _session, socket) do
    if connected?(socket) do
      Process.send_after(self(), :tick, 1000)
    end

    socket = assign(socket, time: current_time())

    {:ok, socket}
  end

  def render(assigns) do
    ~H"""
    <p id="time" class="text-6xl text-center">
      <%= Calendar.strftime(@time, "%_I:%M:%S %p") %>
    </p>
    """
  end

  def handle_info(:tick, socket) do
    Process.send_after(self(), :tick, 1000)

    socket = assign(socket, time: current_time())

    {:noreply, socket}
  end

  defp current_time() do
    DateTime.utc_now()
  end
end

Now, you should be able to run mix phx.server in the terminal and visit http://localhost:4000/clock in your web browser to see the current time, in the UTC time zone, updating every second.

Since Elixir isn’t aware of any time zones other than UTC, we can only display the current time in the UTC time zone. DateTime.now/2 accepts a time zone argument, but by default, it can only handle the “Etc/UTC” time zone. What we’d like is to obtain the time zone the user is in and display the current time in their time zone.

Using a Time Zone Database

Elixir isn’t aware of time zones other than UTC partly because time zones change on occasion, and the Elixir core team doesn’t want to have to release a new version of Elixir to accommodate each change.

We can make Elixir time-zone aware by using an external library. There are libraries that have a large API for creating, changing, and comparing moments in time – in particular, Timex is very popular – but we don’t need anything other than a time zone database and DateTime.now/2 for our app. We’ll use the comparatively minimal library tz to bring in time zone support.

We’ll set up tz’s time zone database to be used globally across our app. First, add the :tz dependency to mix.exs:

  defp deps do
    [
      ...
-     {:bandit, "~> 1.2"}
+     {:bandit, "~> 1.2"},
+     {:tz, "~> 0.27"}
    ]
  end

Then run mix deps.get.

Then, in config/config.exs, configure Elixir to use the time zone database we’ve just installed:

+ config :elixir, :time_zone_database, Tz.TimeZoneDatabase

That’s it! Now Elixir can handle the world’s time zones.

The first argument to DateTime.now/2 is the name of a time zone, such as “Etc/UTC” or “America/New_York”. With Elixir configured to use the tz time zone database, we can use DateTime.now with a single time zone argument to produce the current time in other time zones. You can run iex -S mix to verify this in the console:

iex(1)> DateTime.now("Europe/London")
{:ok, #DateTime<2024-08-14 16:43:15.285929+01:00 BST Europe/London>}
iex(2)> DateTime.now("Australia/Eucla")
{:ok, #DateTime<2024-08-15 00:28:19.787520+08:45 +0845 Australia/Eucla>}

Displaying Time in the User’s Time Zone

Now all we need is the user’s time zone. We can’t get it directly from Elixir, because the Elixir code running on the client is compiled on the server. Fortunately, JavaScript gives us functions to get locale information from the user’s browser. In our front-end JavaScript, we can use Intl.DateTimeFormat().resolvedOptions().timeZone, which is supported by almost all browsers in use today, to get the user’s time zone.

In assets/js/app.js, we will query the browser for the user’s time zone and pass the time zone into the LiveSocket declaration:

  let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
+ let timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
  let liveSocket = new LiveSocket("/live", Socket, {
    longPollFallbackMs: 2500,
-   params: {_csrf_token: csrfToken}
+   params: {_csrf_token: csrfToken, timeZone: timeZone}
})

In our LiveView, we will fetch the time zone information from the socket using get_connect_params. get_connect_params can only be called inside the mount() function, and only returns information when the socket is connected:

  def mount(_params, _session, socket) do
-    if connected?(socket) do
-      Process.send_after(self(), :tick, 1000)
-    end
-
-    socket = assign(socket, time: current_time())
+    socket =
+      if connected?(socket) do
+        Process.send_after(self(), :tick, 1000)
+
+        time_zone = get_connect_params(socket)["timeZone"]
+        time = current_time(time_zone)
+
+        assign(socket,
+          time_zone: time_zone,
+          time: time
+        )
+      else
+        assign(socket, time: DateTime.utc_now())
+      end

    {:ok, socket}
  end

And we’ll use the time zone we fetched to calculate the current time:

  def handle_info(:tick, socket) do
    Process.send_after(self(), :tick, 1000)

-    socket = assign(socket, time: current_time())
+    socket = assign(socket, time: current_time(socket.assigns.time_zone))

    {:noreply, socket}
  end

-  defp current_time() do
-    DateTime.utc_now()
+  defp current_time(time_zone) do
+    case DateTime.now(time_zone) do
+      {:ok, datetime} -> datetime
+      {:error, _reason} -> DateTime.utc_now()
+    end
  end

Now the clock is displayed in the user’s time zone.

This implementation is suitable for our relatively simple app, where we get the user’s time zone on LiveView mount and we don’t look for it to change. If we wanted to get information from the user that might update during the session, such as real-time geolocation information, then an approach using hooks might be more appropriate – but that’ll have to wait for a future blog post.

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box