How to use the datetime-local input type with timezone in a Phoenix LiveView
HTML provides an input type datetime-local that allows the user to select a date and time. This is great for creating and editing date and time values but returns the date and time in the user’s local timezone.
<.input type="datetime-local" field={@form[:datetime]} />
A component and hook to set the user’s timezone
The following component creates a hidden input with a Phoenix LiveView hook that sets the value to the user’s timezone.
defmodule MyAppWeb.CoreComponents do
# ...
attr :id, :any, default: "timezone"
attr :name, :any, default: "timezone"
attr :field, Phoenix.HTML.FormField,
doc: "a form field struct retrieved from the form, for example: @form[:email]"
def timezone(assigns) do
assigns = assigns
|> assign(id: get_in(assigns, [:field, :id]) || assigns.id)
|> assign(name: get_in(assigns, [:field, :name]) || assigns.name)
~H"""
<input
type="hidden"
name={@name}
id={@id}
phx-update="ignore"
phx-hook="TimezoneInput"
/>
"""
end
# ...
end
// app.js
const hooks = {
TimezoneInput: {
mounted() {
this.el.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
},
},
}
let liveSocket = new LiveSocket('/live', Socket, { hooks });
Now you can use the component in your LiveView within a form.
This will return params with the selected date and time along with the user’s timezone, e.g. Africa/Johannesburg.
<.input type="datetime-local" field={@form[:datetime]} />
<.timezone field={@form[:timezone]} />
When the request comes into the LiveView, you can use the timezone to get the date and time in the user’s timezone.
def handle_event("save", params, socket) do
# Get the date and time without seconds (e.g. "2024-08-21T14:58")
<<datetime::binary-size(16), _::binary>> = params["datetime"]
# Parse the date and time (e.g. {:ok, ~N[2024-08-21 14:58:00]}
{:ok, datetime} = NaiveDateTime.from_iso8601(datetime <> ":00Z")
# Set the timezone (e.g. {:ok, #DateTime<2024-08-21 14:58:00+02:00 SAST Africa/Johannesburg>})
{:ok, datetime} = DateTime.from_naive(datetime, params["timezone"])
# Do something with the datetime
# ...
{:noreply, socket}
end