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:
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 withDate.to_iso8601(datetime)
.data-value
needs to be the full ISO 8601 string representation of the date time.x-data
initializes a JavaScriptDate
object fromdata-value
.:value
replaces the initial UTC-based date with the correct date using the user’s time zone. Note the use of thesv-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 withdatetime |> 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 JavaScriptDate
object fromdata-value
.:value
replaces the initial UTC-based time with the correct time using the user’s time zone. Once again we’re usingsv-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!