I wanted to format date/time with user's time zone. Here is what I learned.
Fetching time zone and locale
Browser side
Immediately after the initial render, we want to get the information on local time zone and locale from the browser and include them in the live socket params so that we can receive them in our LiveView process when the client-server connection is established.
Here are some convenient functions:
In /assets/js/app.js
, we can simply add key-value pairs to the live socket params. Then we will be able to access them when the client is connected to the LiveView.
-let liveSocket = new LiveSocket('/live', Socket, { params: { _csrf_token: csrfToken } });
+let liveSocket = new LiveSocket('/live', Socket, {
+ params: {
+ _csrf_token: csrfToken,
+ locale: Intl.NumberFormat().resolvedOptions().locale,
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ timezone_offset: -(new Date().getTimezoneOffset() / 60),
+ },
+});
[info] CONNECTED TO Phoenix.LiveView.Socket in 112µs
Transport: :websocket
Serializer: Phoenix.Socket.V2.JSONSerializer
Parameters: %{"_csrf_token" => "Ay8cCDsHZCFYBicSKTMHfi5EIjowK3sJHWHrqqVH4hcboKI8a1v_wB4g",
"_mounts" => "0",
"_track_static" => %{"0" => "http://localhost:4000/css/app.css",
"1" => "http://localhost:4000/js/app.js"},
"locale" => "en-US",
"timezone" => "America/New_York",
"timezone_offset" => "-5",
"vsn" => "2.0.0"}
Server side (LiveView)
Now we want to fetch the time zone and locale from the socket params. One important thing is the socket params is only remain available during mount. Also we need to provide default values because the browser-dereived information is not available as of the initial render.
defmodule MnishiguchiWeb.TimezoneLive do
use MnishiguchiWeb, :live_view
@default_locale "en"
@default_timezone "UTC"
@default_timezone_offset 0
@impl true
def mount(_params, _session, socket) do
socket =
socket
|> assign_locale()
|> assign_timezone()
|> assign_timezone_offset()
{:ok, socket}
end
defp assign_locale(socket) do
locale = get_connect_params(socket)["locale"] || @default_locale
assign(socket, locale: locale)
end
defp assign_timezone(socket) do
timezone = get_connect_params(socket)["timezone"] || @default_timezone
assign(socket, timezone: timezone)
end
defp assign_timezone_offset(socket) do
timezone_offset = get_connect_params(socket)["timezone_offset"] || @default_timezone_offset
assign(socket, timezone_offset: timezone_offset)
end
...
Formatting datetime
Once the time zone and locale are stored in our LiveView process, we want to transform the datetime values into the format that is friendly to the user. There are two convenient libraries for this purpose.
-
Timex - a rich, comprehensive Date/Time library for Elixir projects, with full timezone support via the
:tzdata
package - Cldr - an Elixir library for the Unicode Consortium’s Common Locale Data Repository (CLDR)
We add these librarie to our mix.exs
:
defp deps do
[
...
+ {:timex, "~> 3.6"},
+ {:ex_cldr_dates_times, "~> 2.0"},
...
]
end
then run mix deps.get
.
According to the Cldr library documentation, all we need to set it up is just create a module like this:
defmodule Mnishiguchi.Cldr do
@default_locale "en"
@default_timezone "UTC"
@default_format :long
use Cldr,
locales: ["en", "ja"],
default_locale: @default_locale,
providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime]
@doc """
Formats datatime based on specified options.
## Examples
iex> format_time(~U[2021-03-02 22:05:28Z], locale: "ja", timezone: "Asia/Tokyo")
"2021年3月3日 7:05:28 JST"
iex> format_time(~U[2021-03-02 22:05:28Z], locale: "ja", timezone: "America/New_York")
"2021年3月2日 17:05:28 EST"
iex> format_time(~U[2021-03-02 22:05:28Z], locale: "en-US", timezone: "America/New_York")
"March 2, 2021 at 5:05:28 PM EST"
# Fallback to ISO8601 string.
iex> format_time(~U[2021-03-02 22:05:28Z], timezone: "Hello")
"2021-03-02T22:05:28+00:00"
"""
@spec format_time(DateTime.t(), nil | list | map) :: binary
def format_time(datetime, options \\ []) do
locale = options[:locale] || @default_locale
timezone = options[:timezone] || @default_timezone
format = options[:format] || @default_format
cldr_options = [locale: locale, format: format]
with time_in_tz <- Timex.Timezone.convert(datetime, timezone),
{:ok, formatted_time} <- __MODULE__.DateTime.to_string(time_in_tz, cldr_options) do
formatted_time
else
{:error, _reason} ->
Timex.format!(datetime, "{ISO:Extended}")
end
end
Then we can that format_time
function anywhere.
Adding loading icon
This is technically optional, but because we do not know the browser's information as of the initial render, we have to fallback to default values. So the use will see the weird effect of time format changing from the default format to local format immediately when the connection is established.
This guy uses a different approach to this issue but I chose to simply hide the contents until the LiveView is connected. I believe the UI will still look natural by showing a nice loading icon. I found the Single Element CSS Spinners library handy.
<%= unless connected?(@socket) do %>
<div style="min-height:90vh">
<div class="loader">Loading...</div>
</div>
<% else %>
<!-- contents -->
<% end %>
Top comments (1)
I made a similar solution except with alpine js and a live view helper component to use the locale info in the browser medium.com/quiqup-engineering/disp...