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?

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