Andrew Timberlake Andrew Timberlake

Hi, I’m Andrew, a programer and entrepreneur from South Africa, founder of Sitesure for monitoring websites, APIs, and background jobs.
Thanks for visiting and reading.


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
21 Aug 2024