Timezones in AngularJS Apps
Who am I?
- Full stack web developer (currently Node and AngularJS)
- @ajmueller on GitHub and Twitter
- CTO of Spork Bytes
Displaying dates in JS applications
- Usually displayed alongside comments, posts, etc.
- Dates are stored in UTC time in databases*.
- Libraries and filters use the user's browser's timezone by default for display.
- What if you need to display a time in another timezone?
*At least it's the default in Postgres. Other databases may differ.
Our data
- Client locations are stored with human-readable metadata (street address, city, state, ZIP)
- Events have a date and time associated with them and are associated with a client location
- All client locations are in the Portland area
Our problem
- We send out events for approval, but those doing approval aren't always in the Portland area
- If an event occurs at 12pm in Portland and someone in New York reviews the event, it would say 3pm
- This is a confusing UX and unsurprisingly lead to a lot of questions
Before solving the problem
A little background on timezones
Timezones can be expressed in a few ways
- TZ name, e.g. America/Los_Angeles
- Abbreviation, e.g. PDT or PST
- UTC offset, e.g. -0700 or -0800
One location can have multiple expressions of its timezone
For Portland, it can be:
- America/Los_Angeles - always
- PDT - during daylight time (spring and summer)
- PST - during standard time (fall and winter)
- UTC offset of -0700 - during daylight time
- UTC offset of -0800 - during standard time
What does this mean?
Timezone is a product of location, date, and time
So which representation do we need?
- AngularJS's date filter and ng-model-options recognize UTC, GMT, and the continental US abbreviations, but no others
- However, they do recognize all offsets
- How can we calculate the offset?
Moment Timezone to the rescue
- Moment.js is the de facto standard library for handling dates in JavaScript
- Moment Timezone is a timezone-aware version
- Given a date and TZ name, it can calculate offset
- All we need is the TZ name, so how do we get it?
Enter Google Maps!
- The Time Zone API can retrieve TZ name, but requires lat/long coordinates to do so. We have addresses.
- The Geocoding API can convert an address to lat/long coordinates.
- What would we do without Google? Make our own haircut appointments?!
The Plan
Server side
- Create geocoding middleware to convert address data to lat/long coordinates
- Create timezone middleware to convert lat/long coordinates to TZ name
- Save the lat/long coordinates and the TZ name in the location table
Client side
- Use Moment Timezone, the location TZ name, and the chosen date to get the offset
- Use the offset in ng-model-options of date picker to choose the time in the timezone of the event
- Save the offset with the event for future display
Let's write some code!
Geocoding middleware
const googleMapsClient = require('@google/maps').createClient({
key: process.env.GOOGLE_API_KEY,
Promise: Promise
});
module.exports = async (req, res, next) => {
try {
const address = `${req.body.streetAddress} ${req.body.streetAddress2 || ''} ${req.body.city}, ${req.body.state} ${req.body.zipCode}`;
const geocodedAddress = await googleMapsClient.geocode({ address }).asPromise();
if (geocodedAddress) {
const coordinates = geocodedAddress.json.results[0].geometry.location;
req.body.location = {
type: 'Point',
coordinates: [
coordinates.lat,
coordinates.lng
],
crs: {
type: 'name',
properties: {
name: 'EPSG:4326'
}
}
};
}
return next();
}
catch(err) {
next(err);
}
};
Timezone middleware
const googleMapsClient = require('@google/maps').createClient({
key: process.env.GOOGLE_API_KEY,
Promise: Promise
});
module.exports = async (req, res, next) => {
try {
const location = req.body.location.coordinates;
const language = 'en';
const timezoneName = await googleMapsClient.timezone({ location, language }).asPromise();
if (timezoneName.json) {
req.body.timezone = timezoneName.json.timeZoneId;
}
return next();
}
catch(err) {
next(err);
}
};
Location route and controller
// wherever you put your routes
const geocode = require('../path/to/middleware/geocode');
const timezone = require('../path/to/middleware/timezone');
app.post('/api/locations/', [geocode, timezone], createLocation);
// wherever you put your controllers
const { Location } = require('../path/to/models');
async function createLocation(req, res) {
try {
const location = await Location.create(req.body);
res.status(201).json(location);
}
catch(err) {
console.log(err);
}
}
AngularJS templates
<!-- Input for selecting date -->
<div kcd-recompile="$ctrl.event.timezoneOffset">
<md-input-container class="md-block">
<label>Date</label>
<input
type="text"
mdc-datetime-picker
ng-model="$ctrl.event.datetime"
ng-model-options="{ timezone: $ctrl.event.timezoneOffset }"
date="true"
time="true"
short-time="true"
format="MMM D, YYYY h:mm a Z">
</md-input-container>
</div>
<!-- Filtering date for output -->
<p>{{ $ctrl.event.datetime.toISOString() | date:"EEEE, MMMM d, y 'at' h:mma":$ctrl.event.timezoneOffset }}</p>
AngularJS controller
ctrl.setTimezoneOffset = function() {
var datetime = ctrl.event.datetime;
var timezoneName = ctrl.locations.find(function(location) {
return location.id === ctrl.event.LocationId;
}).timezone;
ctrl.event.timezoneOffset = moment(datetime).tz(timezoneName).format('ZZ');
// convert the datetime to the new timezone, if one's been chosen
if (datetime) {
ctrl.event.datetime = moment(datetime).tz(timezoneName);
}
};
Full example project
But Alex, AngularJS is so <some year before current year>. How do I do this for <currently hot JS framework>?
Angular (2+)
- You can use offset with date filter
- ngModelOptions doesn't support timezone out of the box anymore
- Datetime picker situation is limited with no apparent support for choosing times at another timezone
React, Vue, etc.
- There is a timezone-aware picker for Vue and a popular React picker has had many requests for timezones.
- There are no built-in date filters like with Angular(JS), so just use Moment Timezone!
- No need for offset like in Angular(JS). Use TZ name from the location.
Why all the extra work for Angular(JS) then?
- Angular(JS) has a powerful filtering system and I wanted to take advantage of that
- The date filter requires offset for full coverage of timezones
- It's definitely possible to just use Moment Timezone and TZ name for display with Angular(JS), too
Questions? Comments?
Timezones in AngularJS Apps
By Alex Mueller
Timezones in AngularJS Apps
Times in applications usually need to be expressed in the user's timezone, but what if you need to express it in the timezone of another location? Learn how to use location metadata, Google Services, and Moment Timezone.js to output dates and times for another timezone in AngularJS applications. This was created for a Meetup presentation - https://www.meetup.com/Front-End-PDX/events/248792918/
- 885