Published on Sunday, 2021-03-21 at 22:00 UTC

Localized Date and Time Inputs With Alpine.js

Learn how to build datetime inputs with time zone support with Alpine.js

Time zones. Phew. If you’re a developer, you’re probably already exasperated after reading just this one word.

Time zones are notoriously complicated to handle. There are almost 40 different Local Times worldwide and they keep changing constantly. The tz database, probably your best chance at handling time zones in an application, includes almost 400 distinct time zones.

But what makes time zones so complicated and, more importantly, can we cheat and make time zones easy, actually?

Before You Continue Reading this Article …

This article is based on learnings from building Keila, a free/libre Open Source

alternative to MailChimp, written in Elixir.

Be sure to check it out at keila.io!

A Quick Primer on UTC

Coordinated Universal Time, or UTC for short, is not a time zone. Instead, it’s a time standard. It’s based on the solar time at 0° longitude. Since the 0th longitudinal meridian is defined to be at the location of the Royal Observatory in Greenwich, England, UTC is sometimes also incorrectly referred to as GMT (Greenwich Mean Time).

UTC is the global standard for time and it is used extensively in the IT world. So just like it’s a global convention that the Prime Meridian is located in Greenwich, it’s a global convention to use the solar time of that same place as an international reference point.

Why Are Time Zones so Complicated?

All time zones are defined in terms of their UTC offset. For example, the time zone in Germany in winter is Central European Time with a UTC offset of + 01:00. In summer it’s Central European Summer Time with a UTC offset of + 02:00.

On the other side of the Greenwich meridian, the time zone in Lima, Peru is Peru Time with a UTC offset of - 05:00 year-round.

Central European Time and Central European Summer Time apply in many European countries, including Germany, Norway, Belgium, France, Italy, and Spain.

But what about other countries at the same longitudes? After all, solar time in Tunis, the capital of Tunisia, is the same as it is in Hamburg, Germany. So does Tunisia share the same time zone with Germany? The answer is: Yes … sometimes.

Since every country has the right to determine which zime zones to follow and whether or not they want to apply daylight saving time, there is no guarantee that countries on the same longitude always share the same time zone. In the case of Tunisia, they have decided to do away with daylight saving time and instead follow Central European Time all year round. In summer, Tunisia is an hour behind Germany. In winter, clocks in the African country are in sync with many European nations.

For that reason, instead of referring to time zones by their name, it makes much more sense to refer to their geographical locations. So when you’re talking about time in Tunisia, referring to the zime zone as Africa/Tunis will avoid the confusion referring to Central European Time might cause.

This pattern of referring to zimezones as Continent/City is the convention used by the tz database, a collaborative and continuous effort to document all time zones and time zone changes in the world. It’s the basis for time zone calculations in many programming languages and must be updated regularly.

Let the Browser Handle Time Zones!

Now, wouldn’t it be nice if we didn’t have to worry about time zones at all? If we could stick to just using UTC in our application?

Unfortunately, an application doing that would not be very popular with users. Nobody wants to constantly calculate UTC offsets in their head!

Fortunately, with the advances of JavaScript APIs, we can leave most of the heavy-lifting to our users’ browsers. And using Alpine.js, it’s now easier than ever.

No need for JQuery date picker plugins of yore.

With Alpine.js, we can follow the principle of progressive enhancement when it comes to date times. Users without JavaScript support will see the UTC representation - which is still useful information - while those with JavaScript will see a local representation.

Let’s start with the case in which we just want to display a date time. All we need to do is add two Alpine.js attributes to a wrapper element: x-data initializes the Alpine.js component and loads the UTC representation into a JavaScript Date object. Date.toLocaleString() then uses the browser’s knowledge about the user’s system configuration to turn the Date object into a localized string.

This functionality is extremely well supported by browsers and even works in IE 6.

This event is scheduled for
<span
  x-data="{date: new Date($el.innerText)}"
  x-text="date.toLocaleString()"
  >2021-03-14T12:00Z</span
>

Date.toLocaleString() does more than just apply the correct timezone. It also applies the user’s language and internationalization settings. So a user in the US would see a string like 3/14/2021, 12:00:00 PM while someone in Germany would see the date represented as 14.3.2021, 12:00:00. If you want to avoid that, you can specify the optional locale parameter. A good fallback option for the locale is sv-en (i.e. English in Sweden) because it’s a date time format following ISO 8601, resulting in something like 2021-03-14 12:00:00.

You can also further customize the date display by specifying the options parameter.

Here’s a version that I like to use for easy-to-read datetimes:

date.toLocaleString(undefined, {
  weekday: "short",
  year: 'numeric',
  month: 'short',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit'
})

This will use the user’s locale (setting it to undefined makes the Browser choose the best fit) and then generate a representation like Sun, Mar 14, 2021, 12:00 PM in the US, So., 14. März 2021, 12:00 in Germany, or 2021年3月14日周三下午12:00 in China.

Date Time Inputs

Handling date time input is slightly more complex. By now, browsers have evolved support for date and time inputs, but alas, they are not able to handle time zones for us.

This means when it comes to handling date time input, we still need some support from our application.

But let’s start once again with UTC and use Alpine.js for progressive enhancement.

To give you an idea where we’re going, here’s the solution I built for the Schedule Campaign dialog in Keila:

Splitting the input into discrete date and time inputs allows us to take advantage of browsers’ built-in support for picking times and dates. No need for JQuery date picker plugins of yore.

And here’s the code:

<h1>Select a date and time:</h1>

<label for="date">Date</label>
<input
    id="date"
    name="datetime[date]"
    type="date"
    value="2021-03-14" 
    data-value="2021-03-14T12:00Z"
    x-data="{date: new Date($el.dataset.value)}"
    :value="date.toLocaleDateString('sv-en')"
>

<label for="time">Time</label>
<input
    id="time"
    name="datetime[time]"
    type="time"
    value="12:00:00"
    data-value="2021-03-14T12:00Z"
    x-data="{date: new Date($el.dataset.value)}"
    :value="date.toLocaleTimeString('sv-en')"
>

<label for="timezone">Timezone</label>
<input
  id="timezone"
  name="datetime[timezone]"
  value="Etc/UTC"
  x-data="{timeFormat: Intl.DateTimeFormat().resolvedOptions()}"
  :value="timeFormat.timeZone"
  readonly
>

Let’s go through these inputs step by step.

The Date Input

First we have the type="date" input. Specifying the input type makes the browser use its native date picking widget. This means we get great support for mobile browsers for free!

If your input is supposed to have a default value, we have to add a few more attributes:

  • value needs to be the UTC date representation of our date time. In Elixir, that could be achieved with Date.to_iso8601(datetime).
  • data-value needs to be the full ISO 8601 string representation of the date time.
  • x-data initializes a JavaScript Date object from data-value.
  • :value replaces the initial UTC-based date with the correct date using the user’s time zone. Note the use of the sv-en locale to get the correct format.

The Time Input

Next we have the type="time" input. Just like with our date input, this input type has good support across all browsers and will present the user with a convenient time picker widget.

If your input is supposed to have a default value, we have to add a few more attributes:

  • value needs to be the UTC time representation of our date time. In Elixir, that could be achieved with datetime |> DateTime.truncate(:second) |> Time.to_iso8601().
  • data-value needs to be the full ISO 8601 string representation of the date time.
  • x-data initializes a JavaScript Date object from data-value.
  • :value replaces the initial UTC-based time with the correct time using the user’s time zone. Once again we’re using sv-en to ensure the correct format.

The Time Zone Input

Finally, we want to make sure to avoid user confusion. So we tell them which time zone we’re treating their input as. For this, we add a read-only input that defaults to displaying Etc/UTC, the tz database name for UTC and use Alpine.js to display the user’s actual time zone.

Putting it back together on the server

Now the point has come where we can’t avoid handling time zones in our application. Fortunately, it only has to happen at the controller level where we can quickly transform the data from our split-in-three date time input into a sane UTC representation. We just need to stitch date and time back together, add our time zone information and shift everything back to UTC.

Here is what that code might look like in Elixir – it is going to be something similar in your language of choice:

with {:ok, date} <- Date.from_iso8601(params["date"]),
     {:ok, time} <- Time.from_iso8601(params["time"] <> ":00"),
     timezone <- params["timezone"],
     {:ok, datetime} <- DateTime.new(date, time, timezone) do
  DateTime.shift_zone!(datetime, "Etc/UTC")
else
  _ -> nil
end

There you have it! Handling date times and time zones in web applications? Done!