Timezones in AngularJS Apps

Who am I?

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

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 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);
	}
}

GitHub - route

GitHub - controller

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>

Note use of kcd-recompile directive.  ng-model-options doesn't allow for two-way binding.

GitHub - input

GitHub - output

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.

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?

Made with Slides.com